n8n-nodes-bgos 1.4.3 → 1.6.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.
@@ -46,6 +46,7 @@ class BGOSAction {
46
46
  { name: 'Chat', value: 'chat' },
47
47
  { name: 'File', value: 'file' },
48
48
  { name: 'Message', value: 'message' },
49
+ { name: 'Peer Agent', value: 'peer' },
49
50
  ],
50
51
  default: 'message',
51
52
  },
@@ -199,6 +200,123 @@ class BGOSAction {
199
200
  default: 'answerCallback',
200
201
  required: true,
201
202
  },
203
+ // ── Peer Agent operations ─────────────────────────────────────────────
204
+ {
205
+ displayName: 'Operation',
206
+ name: 'operation',
207
+ type: 'options',
208
+ noDataExpression: true,
209
+ displayOptions: { show: { resource: ['peer'] } },
210
+ options: [
211
+ {
212
+ name: 'List Peers',
213
+ value: 'listPeers',
214
+ description: "List the user's other assistants visible to this agent. Each peer carries `introduced` (true if you may message them right now) and `expiresAt` for ephemeral allow-once introductions.",
215
+ action: 'List peers',
216
+ },
217
+ {
218
+ name: 'Send to Peer',
219
+ value: 'sendToPeer',
220
+ description: "Send a message into another assistant's side-thread for the user. Optional waitForReply blocks (deadline polling) until the peer replies. Returns { status, sideThreadChatId, messageId, reply? }. Status='requires_introduction' means the user has not enabled this direction yet.",
221
+ action: 'Send to peer',
222
+ },
223
+ {
224
+ name: 'Mark Side-Thread Complete',
225
+ value: 'markPeerThreadComplete',
226
+ description: 'Write the one-line synthesis summary that flips the SideConversationCard from live to completed-collapsed. Only the chat-owner agent (the one whose primary chat hosts the parent message) may call this.',
227
+ action: 'Mark side-thread complete',
228
+ },
229
+ ],
230
+ default: 'listPeers',
231
+ required: true,
232
+ },
233
+ // ── Peer: parameters ──────────────────────────────────────────────────
234
+ {
235
+ displayName: 'Caller Assistant ID',
236
+ name: 'callerAssistantId',
237
+ type: 'string',
238
+ default: '',
239
+ placeholder: '820',
240
+ description: 'The assistant id of THIS agent (the one making the peer call). Required for all peer operations.',
241
+ displayOptions: { show: { resource: ['peer'] } },
242
+ required: true,
243
+ },
244
+ {
245
+ displayName: 'Target Assistant ID',
246
+ name: 'targetAssistantId',
247
+ type: 'string',
248
+ default: '',
249
+ placeholder: '821',
250
+ description: 'The assistant id of the peer being messaged.',
251
+ displayOptions: {
252
+ show: { resource: ['peer'], operation: ['sendToPeer'] },
253
+ },
254
+ required: true,
255
+ },
256
+ {
257
+ displayName: 'Parent Message ID',
258
+ name: 'parentMessageId',
259
+ type: 'string',
260
+ default: '',
261
+ placeholder: '9991',
262
+ description: 'The message id in YOUR chat that anchors the side conversation. The frontend renders the SideConversationCard against this message.',
263
+ displayOptions: {
264
+ show: {
265
+ resource: ['peer'],
266
+ operation: ['sendToPeer', 'markPeerThreadComplete'],
267
+ },
268
+ },
269
+ required: true,
270
+ },
271
+ {
272
+ displayName: 'Text',
273
+ name: 'peerText',
274
+ type: 'string',
275
+ typeOptions: { rows: 4 },
276
+ default: '',
277
+ description: 'Message body to send to the peer.',
278
+ displayOptions: {
279
+ show: { resource: ['peer'], operation: ['sendToPeer'] },
280
+ },
281
+ required: true,
282
+ },
283
+ {
284
+ displayName: 'Wait for Reply',
285
+ name: 'peerWaitForReply',
286
+ type: 'boolean',
287
+ default: false,
288
+ description: 'Block until the peer replies (their reply must include replyToId pointing to your sent message). Default: 60s timeout, max 600s.',
289
+ displayOptions: {
290
+ show: { resource: ['peer'], operation: ['sendToPeer'] },
291
+ },
292
+ },
293
+ {
294
+ displayName: 'Timeout (Seconds)',
295
+ name: 'peerTimeoutSeconds',
296
+ type: 'number',
297
+ default: 60,
298
+ typeOptions: { minValue: 1, maxValue: 600 },
299
+ description: 'How long to wait for the peer reply.',
300
+ displayOptions: {
301
+ show: {
302
+ resource: ['peer'],
303
+ operation: ['sendToPeer'],
304
+ peerWaitForReply: [true],
305
+ },
306
+ },
307
+ },
308
+ {
309
+ displayName: 'Summary',
310
+ name: 'peerSummary',
311
+ type: 'string',
312
+ default: '',
313
+ typeOptions: { rows: 3 },
314
+ description: 'One-line synthesis of what the peer accomplished. Shown to the user as the final state of the SideConversationCard.',
315
+ displayOptions: {
316
+ show: { resource: ['peer'], operation: ['markPeerThreadComplete'] },
317
+ },
318
+ required: true,
319
+ },
202
320
  // ── Send a Message: required fields ───────────────────────────────────
203
321
  {
204
322
  displayName: 'Assistant ID',
@@ -275,6 +393,66 @@ class BGOSAction {
275
393
  ],
276
394
  description: 'Rule: If the message is NOT a direct reply to something the user just typed — cron, scheduler, external webhook, unprompted check-in — use INLINE. If the user just sent a message within the last ~2 min and you want their full attention, MODAL is acceptable. When in doubt, pick INLINE — it never interrupts. Max 6 options for inline.',
277
395
  },
396
+ {
397
+ displayName: 'From Agent',
398
+ name: 'fromAgent',
399
+ type: 'collection',
400
+ placeholder: 'Add Field',
401
+ default: {},
402
+ description: 'Set when this message is being sent BY ANOTHER AI AGENT (n8n LLM, external bot, peer BGOS agent) so it renders distinctly in the BGOS chat (cyan bubble + name+avatar header) and the receiving agent\'s webhook payload carries fromAgent + a systemHint telling its LLM "you are talking to another AI, not a human". Hybrid identity: registered peers (peerId or externalId looked up in agent_peers) win; otherwise inline name/color/avatarUrl are used as-is. Sender field stays USER — fromAgent is the third render flag.',
403
+ options: [
404
+ {
405
+ displayName: 'Peer ID',
406
+ name: 'peerId',
407
+ type: 'number',
408
+ default: 0,
409
+ description: 'agent_peers.id from BGOS registry. If set, registry data wins over inline fields.',
410
+ },
411
+ {
412
+ displayName: 'External ID',
413
+ name: 'externalId',
414
+ type: 'string',
415
+ default: '',
416
+ placeholder: 'marketing-agent-001',
417
+ description: 'Stable string id (per BGOS user) used to look up the peer in the registry. Use when you registered the peer by name and don\'t want to track its numeric id.',
418
+ },
419
+ {
420
+ displayName: 'Name',
421
+ name: 'name',
422
+ type: 'string',
423
+ default: '',
424
+ placeholder: 'Marketing Agent',
425
+ description: 'Display name shown in the cyan bubble header. Inline fallback used when no registered peer matches.',
426
+ },
427
+ {
428
+ displayName: 'Color',
429
+ name: 'color',
430
+ type: 'color',
431
+ default: '',
432
+ description: 'Bubble color hex (e.g. #0EA5E9). Inline fallback. Defaults to BGOS cyan when omitted.',
433
+ },
434
+ {
435
+ displayName: 'Avatar URL',
436
+ name: 'avatarUrl',
437
+ type: 'string',
438
+ default: '',
439
+ placeholder: 'https://...',
440
+ description: 'Avatar URL. Inline fallback.',
441
+ },
442
+ {
443
+ displayName: 'Agent Type',
444
+ name: 'type',
445
+ type: 'options',
446
+ default: 'n8n',
447
+ options: [
448
+ { name: 'n8n workflow', value: 'n8n' },
449
+ { name: 'BGOS assistant', value: 'bgos' },
450
+ { name: 'External', value: 'external' },
451
+ ],
452
+ description: 'Origin of the agent. Influences the default systemHint sent to the receiving agent.',
453
+ },
454
+ ],
455
+ },
278
456
  ],
279
457
  },
280
458
  // ── Delete a Message ──────────────────────────────────────────────────
@@ -412,6 +590,66 @@ class BGOSAction {
412
590
  default: 2000,
413
591
  description: 'How often to check for new messages while waiting (default 2 seconds; minimum 1000).',
414
592
  },
593
+ {
594
+ displayName: 'From Agent',
595
+ name: 'fromAgent',
596
+ type: 'collection',
597
+ placeholder: 'Add Field',
598
+ default: {},
599
+ description: 'Identify the sending peer agent so the message renders distinctly (cyan bubble + header) AND the receiving agent\'s webhook payload includes fromAgent + a systemHint. Hybrid: registry (peerId/externalId) > inline (name/color/avatarUrl). The Send-and-Wait poll loop is unaffected — it still reads the next reply from the chat history regardless of fromAgent.',
600
+ options: [
601
+ {
602
+ displayName: 'Peer ID',
603
+ name: 'peerId',
604
+ type: 'number',
605
+ default: 0,
606
+ description: 'agent_peers.id from BGOS registry. Registry wins over inline.',
607
+ },
608
+ {
609
+ displayName: 'External ID',
610
+ name: 'externalId',
611
+ type: 'string',
612
+ default: '',
613
+ placeholder: 'marketing-agent-001',
614
+ description: 'Stable string id used to look up the peer in the registry.',
615
+ },
616
+ {
617
+ displayName: 'Name',
618
+ name: 'name',
619
+ type: 'string',
620
+ default: '',
621
+ placeholder: 'Marketing Agent',
622
+ description: 'Display name. Inline fallback when no registry match.',
623
+ },
624
+ {
625
+ displayName: 'Color',
626
+ name: 'color',
627
+ type: 'color',
628
+ default: '',
629
+ description: 'Bubble color hex e.g. #0EA5E9. Inline fallback.',
630
+ },
631
+ {
632
+ displayName: 'Avatar URL',
633
+ name: 'avatarUrl',
634
+ type: 'string',
635
+ default: '',
636
+ placeholder: 'https://...',
637
+ description: 'Avatar URL. Inline fallback.',
638
+ },
639
+ {
640
+ displayName: 'Agent Type',
641
+ name: 'type',
642
+ type: 'options',
643
+ default: 'n8n',
644
+ options: [
645
+ { name: 'n8n workflow', value: 'n8n' },
646
+ { name: 'BGOS assistant', value: 'bgos' },
647
+ { name: 'External', value: 'external' },
648
+ ],
649
+ description: 'Origin of the agent.',
650
+ },
651
+ ],
652
+ },
415
653
  ],
416
654
  },
417
655
  // ── Ask User Input ────────────────────────────────────────────────────
@@ -758,6 +996,12 @@ class BGOSAction {
758
996
  nodeParams.renderMode = additionalFields.renderMode
759
997
  ? String(additionalFields.renderMode)
760
998
  : 'inline';
999
+ // Forward the From Agent collection block (peerId/externalId/
1000
+ // name/color/avatarUrl/type). Backend's hybrid resolver picks
1001
+ // registry > inline; absence falls through to a normal user msg.
1002
+ if (additionalFields.fromAgent && typeof additionalFields.fromAgent === 'object') {
1003
+ nodeParams.fromAgent = additionalFields.fromAgent;
1004
+ }
761
1005
  }
762
1006
  if (operation === 'deleteMessage') {
763
1007
  nodeParams.deleteMessageId = String(this.getNodeParameter('deleteMessageId', i, '') ?? '');
@@ -781,6 +1025,9 @@ class BGOSAction {
781
1025
  nodeParams.fileId = String(replyAdditional.fileId ?? '');
782
1026
  nodeParams.replyTimeoutSeconds = Number(replyAdditional.replyTimeoutSeconds ?? 600);
783
1027
  nodeParams.replyPollIntervalMs = Number(replyAdditional.replyPollIntervalMs ?? 2000);
1028
+ if (replyAdditional.fromAgent && typeof replyAdditional.fromAgent === 'object') {
1029
+ nodeParams.fromAgent = replyAdditional.fromAgent;
1030
+ }
784
1031
  }
785
1032
  if (operation === 'getChat') {
786
1033
  nodeParams.chatId = String(this.getNodeParameter('chatId', i, '') ?? '');
@@ -41,6 +41,13 @@ type NodeParams = {
41
41
  isPinned?: boolean;
42
42
  replyTimeoutSeconds?: number;
43
43
  replyPollIntervalMs?: number;
44
+ callerAssistantId?: string;
45
+ targetAssistantId?: string;
46
+ parentMessageId?: string;
47
+ peerText?: string;
48
+ peerWaitForReply?: boolean;
49
+ peerTimeoutSeconds?: number;
50
+ peerSummary?: string;
44
51
  [key: string]: unknown;
45
52
  };
46
53
  export declare function handleEventByType(this: IExecuteFunctions, eventType: string, eventData: Record<string, unknown>, nodeParams: NodeParams): Promise<unknown>;
@@ -9,6 +9,38 @@ function getApiOptions(nodeParams) {
9
9
  }
10
10
  return { baseUrl, apiKey: nodeParams.apiKey };
11
11
  }
12
+ /**
13
+ * Strip empty values from the `fromAgent` collection so an empty UI block
14
+ * doesn't accidentally trigger the agent-render path on the backend. Returns
15
+ * undefined when there's nothing meaningful to send.
16
+ */
17
+ function sanitizeFromAgent(raw) {
18
+ if (!raw || typeof raw !== 'object')
19
+ return undefined;
20
+ const obj = raw;
21
+ const peerId = typeof obj.peerId === 'number' && obj.peerId > 0 ? obj.peerId : undefined;
22
+ const externalId = typeof obj.externalId === 'string' && obj.externalId.trim() ? obj.externalId.trim() : undefined;
23
+ const name = typeof obj.name === 'string' && obj.name.trim() ? obj.name.trim() : undefined;
24
+ const color = typeof obj.color === 'string' && obj.color.trim() ? obj.color.trim() : undefined;
25
+ const avatarUrl = typeof obj.avatarUrl === 'string' && obj.avatarUrl.trim() ? obj.avatarUrl.trim() : undefined;
26
+ const type = typeof obj.type === 'string' && obj.type.trim() ? obj.type.trim() : undefined;
27
+ if (!peerId && !externalId && !name && !color && !avatarUrl && !type)
28
+ return undefined;
29
+ const out = {};
30
+ if (peerId !== undefined)
31
+ out.peerId = peerId;
32
+ if (externalId !== undefined)
33
+ out.externalId = externalId;
34
+ if (name !== undefined)
35
+ out.name = name;
36
+ if (color !== undefined)
37
+ out.color = color;
38
+ if (avatarUrl !== undefined)
39
+ out.avatarUrl = avatarUrl;
40
+ if (type !== undefined)
41
+ out.type = type;
42
+ return out;
43
+ }
12
44
  async function handleEventByType(eventType, eventData, nodeParams) {
13
45
  const operation = nodeParams.operation;
14
46
  const assistant = eventData.assistant;
@@ -54,6 +86,7 @@ async function handleEventByType(eventType, eventData, nodeParams) {
54
86
  ?? eventData.renderMode
55
87
  ?? eventData.render_mode
56
88
  ?? 'inline');
89
+ const fromAgent = sanitizeFromAgent(nodeParams.fromAgent);
57
90
  return await techWebhook_1.sendMessageToBackend.call(this, apiOptions, {
58
91
  assistantId: assistantId,
59
92
  chatId: chatId,
@@ -71,6 +104,7 @@ async function handleEventByType(eventType, eventData, nodeParams) {
71
104
  audioMimeType: (eventData.audioMimeType ?? message?.audioMimeType ?? null),
72
105
  audioDuration: (eventData.audioDuration ?? message?.audioDuration ?? null),
73
106
  isMixedAttachments: files.length > 0 && text ? true : (eventData.isMixedAttachments ?? message?.isMixedAttachments ?? null),
107
+ ...(fromAgent ? { fromAgent } : {}),
74
108
  });
75
109
  }
76
110
  case 'deleteMessage': {
@@ -115,7 +149,12 @@ async function handleEventByType(eventType, eventData, nodeParams) {
115
149
  const fileIdParam = nodeParams.fileId?.trim();
116
150
  const files = fileIdParam ? [{ id: fileIdParam }] : [];
117
151
  const sender = nodeParams.sender || 'assistant';
152
+ const fromAgentReply = sanitizeFromAgent(nodeParams.fromAgent);
118
153
  // Step 1: post the outgoing message and capture its id
154
+ // NOTE: fromAgent flows through here — it's persisted on the request
155
+ // message and forwarded on the receiving agent's webhook payload, but
156
+ // does NOT affect the polling loop below (which only reads sentMessageId
157
+ // and the next message's id+text).
119
158
  const sendResult = (await techWebhook_1.sendMessageToBackend.call(this, apiOptions, {
120
159
  assistantId: assistantId,
121
160
  chatId: chatId,
@@ -123,6 +162,7 @@ async function handleEventByType(eventType, eventData, nodeParams) {
123
162
  text,
124
163
  files,
125
164
  isMixedAttachments: files.length > 0 ? true : null,
165
+ ...(fromAgentReply ? { fromAgent: fromAgentReply } : {}),
126
166
  }));
127
167
  const sentMessageId = Number(sendResult?.id
128
168
  ?? sendResult?.message?.id
@@ -457,6 +497,64 @@ async function handleEventByType(eventType, eventData, nodeParams) {
457
497
  error,
458
498
  });
459
499
  }
500
+ // ─────────────────────────────────────────────────────────────────────
501
+ // Peer Agent (cross-channel agent-to-agent) — see
502
+ // docs/superpowers/specs/2026-04-30-agent-to-agent-via-bgos-design.md
503
+ // ─────────────────────────────────────────────────────────────────────
504
+ case 'listPeers': {
505
+ const apiOptions = getApiOptions(nodeParams);
506
+ const callerAssistantIdRaw = nodeParams.callerAssistantId ??
507
+ eventData.assistant_id ??
508
+ assistant?.id;
509
+ const callerAssistantId = Number(callerAssistantIdRaw);
510
+ if (!Number.isFinite(callerAssistantId) || callerAssistantId <= 0) {
511
+ throw new Error('Caller Assistant ID is required for List Peers (the assistant id of THIS agent).');
512
+ }
513
+ return await techWebhook_1.listPeers.call(this, apiOptions, callerAssistantId);
514
+ }
515
+ case 'sendToPeer': {
516
+ const apiOptions = getApiOptions(nodeParams);
517
+ const callerAssistantId = Number(nodeParams.callerAssistantId);
518
+ const targetAssistantId = Number(nodeParams.targetAssistantId);
519
+ const parentMessageId = Number(nodeParams.parentMessageId);
520
+ const text = (nodeParams.peerText ?? '').trim();
521
+ if (!Number.isFinite(callerAssistantId) || callerAssistantId <= 0) {
522
+ throw new Error('Caller Assistant ID is required for Send to Peer.');
523
+ }
524
+ if (!Number.isFinite(targetAssistantId) || targetAssistantId <= 0) {
525
+ throw new Error('Target Assistant ID is required for Send to Peer.');
526
+ }
527
+ if (!Number.isFinite(parentMessageId) || parentMessageId <= 0) {
528
+ throw new Error('Parent Message ID is required for Send to Peer — it anchors the SideConversationCard in your chat.');
529
+ }
530
+ if (!text) {
531
+ throw new Error('Text is required for Send to Peer.');
532
+ }
533
+ return await techWebhook_1.sendToPeer.call(this, apiOptions, callerAssistantId, targetAssistantId, {
534
+ text,
535
+ parentMessageId,
536
+ waitForReply: nodeParams.peerWaitForReply === true,
537
+ timeoutSeconds: typeof nodeParams.peerTimeoutSeconds === 'number'
538
+ ? nodeParams.peerTimeoutSeconds
539
+ : undefined,
540
+ });
541
+ }
542
+ case 'markPeerThreadComplete': {
543
+ const apiOptions = getApiOptions(nodeParams);
544
+ const callerAssistantId = Number(nodeParams.callerAssistantId);
545
+ const parentMessageId = Number(nodeParams.parentMessageId);
546
+ const summary = (nodeParams.peerSummary ?? '').trim();
547
+ if (!Number.isFinite(callerAssistantId) || callerAssistantId <= 0) {
548
+ throw new Error('Caller Assistant ID is required for Mark Side-Thread Complete.');
549
+ }
550
+ if (!Number.isFinite(parentMessageId) || parentMessageId <= 0) {
551
+ throw new Error('Parent Message ID is required for Mark Side-Thread Complete.');
552
+ }
553
+ if (!summary) {
554
+ throw new Error('Summary is required for Mark Side-Thread Complete — this is the one-line synthesis the user sees in the SideConversationCard.');
555
+ }
556
+ return await techWebhook_1.markPeerThreadComplete.call(this, apiOptions, callerAssistantId, parentMessageId, summary);
557
+ }
460
558
  default:
461
559
  throw new Error(`Unsupported operation: ${operation}`);
462
560
  }
@@ -146,3 +146,31 @@ export declare function uploadBinaryToS3(this: IExecuteFunctions, uploadUrl: str
146
146
  * Step 3: Save file metadata to the BGOS backend after a successful S3 upload.
147
147
  */
148
148
  export declare function saveFileMeta(this: IExecuteFunctions, options: BgosApiOptions, userId: string, key: string, type: string, size: number): Promise<FileUploadResult>;
149
+ export interface PeerListItem {
150
+ assistantId: number;
151
+ name: string;
152
+ avatarUrl: string | null;
153
+ color: string | null;
154
+ introduced: boolean;
155
+ expiresAt: string | null;
156
+ }
157
+ export interface SendToPeerResult {
158
+ status: 'sent' | 'requires_introduction';
159
+ sideThreadChatId: number | null;
160
+ messageId: number | null;
161
+ reply: {
162
+ messageId: number;
163
+ text: string | null;
164
+ fromAssistantId: number;
165
+ } | null;
166
+ }
167
+ export declare function listPeers(this: IExecuteFunctions, options: BgosApiOptions, callerAssistantId: number): Promise<PeerListItem[]>;
168
+ export declare function sendToPeer(this: IExecuteFunctions, options: BgosApiOptions, callerAssistantId: number, targetAssistantId: number, body: {
169
+ text: string;
170
+ parentMessageId: number;
171
+ waitForReply?: boolean;
172
+ timeoutSeconds?: number;
173
+ }): Promise<SendToPeerResult>;
174
+ export declare function markPeerThreadComplete(this: IExecuteFunctions, options: BgosApiOptions, callerAssistantId: number, parentMessageId: number, summary: string): Promise<{
175
+ ok: true;
176
+ }>;
@@ -17,6 +17,9 @@ exports.reportCallbackResult = reportCallbackResult;
17
17
  exports.getUploadUrl = getUploadUrl;
18
18
  exports.uploadBinaryToS3 = uploadBinaryToS3;
19
19
  exports.saveFileMeta = saveFileMeta;
20
+ exports.listPeers = listPeers;
21
+ exports.sendToPeer = sendToPeer;
22
+ exports.markPeerThreadComplete = markPeerThreadComplete;
20
23
  function buildHeaders(apiKey) {
21
24
  const headers = { 'Content-Type': 'application/json', Accept: 'application/json' };
22
25
  if (apiKey)
@@ -167,6 +170,12 @@ async function sendMessageToBackend(options, payload) {
167
170
  };
168
171
  if (payload.renderMode)
169
172
  body.renderMode = payload.renderMode;
173
+ // Forward the peer-agent identity block when the workflow set it. Backend's
174
+ // hybrid resolver picks registry vs inline; either form makes the BGOS UI
175
+ // render the cyan agent bubble + name header on this message.
176
+ if (payload.fromAgent && typeof payload.fromAgent === 'object') {
177
+ body.fromAgent = payload.fromAgent;
178
+ }
170
179
  return this.helpers.httpRequest({
171
180
  method: 'POST',
172
181
  url,
@@ -283,3 +292,38 @@ async function saveFileMeta(options, userId, key, type, size) {
283
292
  json: true,
284
293
  });
285
294
  }
295
+ function buildPeerHeaders(apiKey, callerAssistantId) {
296
+ const headers = buildHeaders(apiKey);
297
+ headers['X-Caller-Assistant-Id'] = String(callerAssistantId);
298
+ return headers;
299
+ }
300
+ async function listPeers(options, callerAssistantId) {
301
+ const url = `${options.baseUrl.replace(/\/$/, '')}/api/v1/peers`;
302
+ const result = (await this.helpers.httpRequest({
303
+ method: 'GET',
304
+ url,
305
+ headers: buildPeerHeaders(options.apiKey, callerAssistantId),
306
+ json: true,
307
+ }));
308
+ return Array.isArray(result) ? result : [];
309
+ }
310
+ async function sendToPeer(options, callerAssistantId, targetAssistantId, body) {
311
+ const url = `${options.baseUrl.replace(/\/$/, '')}/api/v1/peers/${encodeURIComponent(String(targetAssistantId))}/send`;
312
+ return this.helpers.httpRequest({
313
+ method: 'POST',
314
+ url,
315
+ headers: buildPeerHeaders(options.apiKey, callerAssistantId),
316
+ body,
317
+ json: true,
318
+ });
319
+ }
320
+ async function markPeerThreadComplete(options, callerAssistantId, parentMessageId, summary) {
321
+ const url = `${options.baseUrl.replace(/\/$/, '')}/api/v1/peers/threads/${encodeURIComponent(String(parentMessageId))}/complete`;
322
+ return this.helpers.httpRequest({
323
+ method: 'POST',
324
+ url,
325
+ headers: buildPeerHeaders(options.apiKey, callerAssistantId),
326
+ body: { summary },
327
+ json: true,
328
+ });
329
+ }
@@ -133,8 +133,46 @@ class BgosTrigger {
133
133
  const rawBody = this.getBodyData();
134
134
  const updates = this.getNodeParameter('updates', 0);
135
135
  const workflowId = String(this.getWorkflow().id ?? 'default');
136
- if (isRateLimited(workflowId))
137
- return { workflowData: [] };
136
+ if (isRateLimited(workflowId)) {
137
+ // Drop the event explicitly with HTTP 429 so the upstream caller
138
+ // (BGOS backend webhook forwarder) sees a real failure and surfaces
139
+ // it in the BGOS forwarder log. Returning empty workflowData with a
140
+ // 200 silently swallowed flood-control drops, which made it
141
+ // impossible to diagnose missing events from a noisy assistant.
142
+ const res = this.getResponseObject();
143
+ try {
144
+ res.status(429).json({
145
+ error: 'rate_limited',
146
+ message: 'BGOS Trigger rate limit exceeded — event dropped.',
147
+ workflowId,
148
+ windowMs: RATE_WINDOW_MS,
149
+ maxEvents: RATE_MAX_EVENTS,
150
+ });
151
+ }
152
+ catch {
153
+ // Older n8n runtimes lack getResponseObject — fall back to the
154
+ // older shape but still expose the drop in workflowData so users
155
+ // can branch on it instead of silently no-op'ing.
156
+ }
157
+ return {
158
+ webhookResponse: {
159
+ status: 429,
160
+ body: 'rate_limited',
161
+ },
162
+ workflowData: [
163
+ [
164
+ {
165
+ json: {
166
+ error: 'rate_limited',
167
+ workflowId,
168
+ windowMs: RATE_WINDOW_MS,
169
+ maxEvents: RATE_MAX_EVENTS,
170
+ },
171
+ },
172
+ ],
173
+ ],
174
+ };
175
+ }
138
176
  if (!rawBody) {
139
177
  return { workflowData: [] };
140
178
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-bgos",
3
- "version": "1.4.3",
3
+ "version": "1.6.0",
4
4
  "description": "n8n community nodes for BGOS (Brand Growth OS) - AI assistant chat platform",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-bgos",
3
- "version": "1.4.3",
3
+ "version": "1.6.0",
4
4
  "description": "n8n community nodes for BGOS (Brand Growth OS) - AI assistant chat platform",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",