agentgate 0.3.1 → 0.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgate",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "API gateway for AI agents with human-in-the-loop write approval",
6
6
  "main": "src/index.js",
@@ -1,5 +1,4 @@
1
1
  import { getAccountCredentials, setAccountCredentials, updateQueueStatus, getQueueEntry } from './db.js';
2
- import { notifyClawdbot } from './notifier.js';
3
2
  import { notifyAgentQueueStatus } from './agentNotifier.js';
4
3
 
5
4
  // Service base URLs
@@ -281,9 +280,6 @@ function finalizeEntry(entryId, status, results) {
281
280
  notifyAgentQueueStatus(updatedEntry).catch(err => {
282
281
  console.error('[agentNotifier] Failed to notify agent:', err.message);
283
282
  });
284
- notifyClawdbot(updatedEntry).catch(err => {
285
- console.error('[notifier] Failed to notify Clawdbot:', err.message);
286
- });
287
283
  }
288
284
 
289
285
  // Execute a single queued entry (batch of requests)
@@ -331,7 +327,7 @@ export async function executeQueueEntry(entry) {
331
327
  headers
332
328
  };
333
329
 
334
- if (req.body && ['POST', 'PUT', 'PATCH'].includes(req.method.toUpperCase())) {
330
+ if (req.body && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method.toUpperCase())) {
335
331
  if (req.binaryBase64) {
336
332
  // Binary data encoded as base64 (for blob uploads)
337
333
  fetchOptions.body = Buffer.from(req.body, 'base64');
@@ -16,6 +16,15 @@ export const serviceInfo = {
16
16
  'GET /api/bluesky/{accountName}/app.bsky.feed.getTimeline',
17
17
  'GET /api/bluesky/{accountName}/app.bsky.feed.getAuthorFeed?actor={handle}',
18
18
  'GET /api/bluesky/{accountName}/app.bsky.actor.getProfile?actor={handle}'
19
+ ],
20
+ writeGuidelines: [
21
+ 'Posts require FACETS for clickable links, mentions, and hashtags - they are NOT auto-detected',
22
+ 'Facet positions use UTF-8 BYTE offsets, not character positions (emoji=4 bytes, em-dash=3 bytes)',
23
+ 'Link facet: { index: { byteStart, byteEnd }, features: [{ $type: "app.bsky.richtext.facet#link", uri: "https://..." }] }',
24
+ 'Mention facet requires DID (resolve via com.atproto.identity.resolveHandle), not handle',
25
+ 'Hashtag facet: tag value should NOT include the # symbol',
26
+ 'Always include "langs" array (e.g. ["en"]) and "createdAt" ISO timestamp',
27
+ 'Use TextEncoder to calculate byte offsets: encoder.encode(text.slice(0, start)).length'
19
28
  ]
20
29
  };
21
30
 
package/src/routes/ui.js CHANGED
@@ -4,13 +4,11 @@ import {
4
4
  setAdminPassword, verifyAdminPassword, hasAdminPassword,
5
5
  listQueueEntries, getQueueEntry, updateQueueStatus, clearQueueByStatus, deleteQueueEntry, getPendingQueueCount, getQueueCounts,
6
6
  listApiKeys, createApiKey, deleteApiKey, updateAgentWebhook, getApiKeyById,
7
- listUnnotifiedEntries,
8
7
  getMessagingMode, setMessagingMode, listPendingMessages, listAgentMessages,
9
8
  approveAgentMessage, rejectAgentMessage, deleteAgentMessage, clearAgentMessagesByStatus, getMessageCounts, getAgentMessage
10
9
  } from '../lib/db.js';
11
10
  import { connectHsync, disconnectHsync, getHsyncUrl, isHsyncConnected } from '../lib/hsyncManager.js';
12
11
  import { executeQueueEntry } from '../lib/queueExecutor.js';
13
- import { notifyClawdbot, retryNotification } from '../lib/notifier.js';
14
12
  import { notifyAgentMessage, notifyMessageRejected, notifyAgentQueueStatus } from '../lib/agentNotifier.js';
15
13
  import { registerAllRoutes, renderAllCards } from './ui/index.js';
16
14
 
@@ -126,11 +124,10 @@ router.get('/', (req, res) => {
126
124
  const hsyncUrl = getHsyncUrl();
127
125
  const hsyncConnected = isHsyncConnected();
128
126
  const pendingQueueCount = getPendingQueueCount();
129
- const notificationsConfig = getSetting('notifications');
130
127
  const messagingMode = getMessagingMode();
131
128
  const pendingMessagesCount = listPendingMessages().length;
132
129
 
133
- res.send(renderPage(accounts, { hsyncConfig, hsyncUrl, hsyncConnected, pendingQueueCount, notificationsConfig, messagingMode, pendingMessagesCount }));
130
+ res.send(renderPage(accounts, { hsyncConfig, hsyncUrl, hsyncConnected, pendingQueueCount, messagingMode, pendingMessagesCount }));
134
131
  });
135
132
 
136
133
  // Register all service routes (github, bluesky, reddit, etc.)
@@ -157,78 +154,9 @@ router.post('/hsync/delete', async (req, res) => {
157
154
  res.redirect('/ui');
158
155
  });
159
156
 
160
- // Notification settings
161
- router.post('/notifications/setup', (req, res) => {
162
- const { url, token, events } = req.body;
163
- if (!url) {
164
- return res.status(400).send('Webhook URL required');
165
- }
166
-
167
- // Parse events - could be comma-separated string or array
168
- let eventList = ['completed', 'failed'];
169
- if (events) {
170
- eventList = Array.isArray(events) ? events : events.split(',').map(e => e.trim());
171
- }
172
-
173
- setSetting('notifications', {
174
- clawdbot: {
175
- enabled: true,
176
- url: url.replace(/\/$/, ''),
177
- token: token || '',
178
- events: eventList,
179
- retryAttempts: 3,
180
- retryDelayMs: 5000
181
- }
182
- });
183
- res.redirect('/ui');
184
- });
185
-
186
- router.post('/notifications/delete', (req, res) => {
187
- deleteSetting('notifications');
188
- res.redirect('/ui');
189
- });
190
-
191
- router.post('/notifications/test', async (req, res) => {
192
- const wantsJson = req.headers.accept?.includes('application/json');
193
- const config = getSetting('notifications');
157
+ // Notification settings removed - using agent-specific webhooks
194
158
 
195
- if (!config?.clawdbot?.enabled || !config.clawdbot.url || !config.clawdbot.token) {
196
- const error = 'Notifications not configured';
197
- return wantsJson
198
- ? res.status(400).json({ success: false, error })
199
- : res.status(400).send(error);
200
- }
201
159
 
202
- try {
203
- const response = await fetch(config.clawdbot.url, {
204
- method: 'POST',
205
- headers: {
206
- 'Authorization': `Bearer ${config.clawdbot.token}`,
207
- 'Content-Type': 'application/json'
208
- },
209
- body: JSON.stringify({
210
- text: '🧪 [agentgate] Test notification - webhook is working!',
211
- mode: 'now'
212
- })
213
- });
214
-
215
- if (response.ok) {
216
- return wantsJson
217
- ? res.json({ success: true })
218
- : res.redirect('/ui?notification_test=success');
219
- } else {
220
- const text = await response.text().catch(() => '');
221
- const error = `HTTP ${response.status}: ${text.substring(0, 100)}`;
222
- return wantsJson
223
- ? res.status(400).json({ success: false, error })
224
- : res.redirect('/ui?notification_test=failed');
225
- }
226
- } catch (err) {
227
- return wantsJson
228
- ? res.status(500).json({ success: false, error: err.message })
229
- : res.redirect('/ui?notification_test=failed');
230
- }
231
- });
232
160
 
233
161
  // Agent Messaging settings
234
162
  router.post('/messaging/mode', (req, res) => {
@@ -354,8 +282,7 @@ router.get('/queue', (req, res) => {
354
282
  entries = listQueueEntries(filter);
355
283
  }
356
284
  const counts = getQueueCounts();
357
- const unnotified = listUnnotifiedEntries();
358
- res.send(renderQueuePage(entries, filter, counts, unnotified.length));
285
+ res.send(renderQueuePage(entries, filter, counts));
359
286
  });
360
287
 
361
288
  router.post('/queue/:id/approve', async (req, res) => {
@@ -417,9 +344,6 @@ router.post('/queue/:id/reject', async (req, res) => {
417
344
  notifyAgentQueueStatus(updated).catch(err => {
418
345
  console.error('[agentNotifier] Failed to notify agent:', err.message);
419
346
  });
420
- notifyClawdbot(updated).catch(err => {
421
- console.error('[notifier] Failed to notify Clawdbot:', err.message);
422
- });
423
347
 
424
348
  const counts = getQueueCounts();
425
349
 
@@ -475,35 +399,15 @@ router.post('/queue/:id/notify', async (req, res) => {
475
399
  const { id } = req.params;
476
400
  const wantsJson = req.headers.accept?.includes('application/json');
477
401
 
478
- const result = await retryNotification(id, getQueueEntry);
479
402
  const updated = getQueueEntry(id);
480
403
 
481
404
  if (wantsJson) {
482
- return res.json({ success: result.success, error: result.error, entry: updated });
405
+ return res.json({ success: true, entry: updated });
483
406
  }
484
407
  res.redirect('/ui/queue');
485
408
  });
486
409
 
487
- // Retry all failed notifications
488
- router.post('/queue/notify-all', async (req, res) => {
489
- const wantsJson = req.headers.accept?.includes('application/json');
490
- const unnotified = listUnnotifiedEntries();
491
410
 
492
- if (unnotified.length === 0) {
493
- return wantsJson
494
- ? res.json({ success: true, count: 0 })
495
- : res.redirect('/ui/queue');
496
- }
497
-
498
- // Batch into single notification
499
- const { notifyClawdbotBatch } = await import('../lib/notifier.js');
500
- const result = await notifyClawdbotBatch(unnotified);
501
-
502
- if (wantsJson) {
503
- return res.json({ success: result.success, error: result.error, count: unnotified.length });
504
- }
505
- res.redirect('/ui/queue');
506
- });
507
411
 
508
412
  // API Keys Management
509
413
  router.get('/keys', (req, res) => {
@@ -580,8 +484,7 @@ router.delete('/keys/:id', (req, res) => {
580
484
 
581
485
  // HTML Templates
582
486
 
583
- function renderPage(accounts, { hsyncConfig, hsyncUrl, hsyncConnected, pendingQueueCount, notificationsConfig, messagingMode, pendingMessagesCount }) {
584
- const clawdbotConfig = notificationsConfig?.clawdbot;
487
+ function renderPage(accounts, { hsyncConfig, hsyncUrl, hsyncConnected, pendingQueueCount, messagingMode, pendingMessagesCount }) {
585
488
  return `<!DOCTYPE html>
586
489
  <html>
587
490
  <head>
@@ -698,61 +601,6 @@ curl -X POST http://localhost:${PORT}/api/queue/github/personal/submit \\
698
601
  </div>
699
602
 
700
603
  <div class="card">
701
- <details ${clawdbotConfig?.enabled ? 'open' : ''}>
702
- <summary>Clawdbot Notifications ${clawdbotConfig?.enabled ? '<span class="status configured">Configured</span>' : ''}</summary>
703
- <div style="margin-top: 16px;">
704
- <div id="notification-feedback"></div>
705
- ${clawdbotConfig?.enabled ? `
706
- <p>Webhook URL: <strong>${clawdbotConfig.url}</strong></p>
707
- <p>Events: <code>${(clawdbotConfig.events || ['completed', 'failed']).join(', ')}</code></p>
708
- <div style="display: flex; gap: 8px; margin-top: 12px;">
709
- <button type="button" class="btn-primary btn-sm" id="test-notification-btn" onclick="testNotification()">Send Test</button>
710
- <form method="POST" action="/ui/notifications/delete" style="margin: 0;">
711
- <button type="submit" class="btn-danger btn-sm">Disable</button>
712
- </form>
713
- </div>
714
- <script>
715
- async function testNotification() {
716
- const btn = document.getElementById('test-notification-btn');
717
- const feedback = document.getElementById('notification-feedback');
718
- btn.disabled = true;
719
- btn.textContent = 'Sending...';
720
- feedback.innerHTML = '';
721
-
722
- try {
723
- const res = await fetch('/ui/notifications/test', {
724
- method: 'POST',
725
- headers: { 'Accept': 'application/json' }
726
- });
727
- const data = await res.json();
728
-
729
- if (data.success) {
730
- feedback.innerHTML = '<div class="success-message" style="margin-bottom: 16px;">✓ Test notification sent!</div>';
731
- } else {
732
- feedback.innerHTML = '<div class="error-message" style="margin-bottom: 16px;">✗ ' + (data.error || 'Failed to send') + '</div>';
733
- }
734
- } catch (err) {
735
- feedback.innerHTML = '<div class="error-message" style="margin-bottom: 16px;">✗ ' + err.message + '</div>';
736
- }
737
-
738
- btn.disabled = false;
739
- btn.textContent = 'Send Test';
740
- }
741
- </script>
742
- ` : `
743
- <p class="help">Send notifications to <a href="https://docs.clawd.bot" target="_blank">Clawdbot</a> when queue items are completed, failed, or rejected.</p>
744
- <form method="POST" action="/ui/notifications/setup">
745
- <label>Webhook URL</label>
746
- <input type="text" name="url" placeholder="https://your-gateway.example.com/hooks/wake" required>
747
- <label>Token</label>
748
- <input type="password" name="token" placeholder="Clawdbot hooks token" required>
749
- <label>Events (comma-separated)</label>
750
- <input type="text" name="events" placeholder="completed, failed, rejected" value="completed, failed, rejected">
751
- <button type="submit" class="btn-primary">Enable Notifications</button>
752
- </form>
753
- `}
754
- </div>
755
- </details>
756
604
  </div>
757
605
  </body>
758
606
  </html>`;
@@ -807,7 +655,7 @@ function renderSetupPasswordPage(error = '') {
807
655
  </html>`;
808
656
  }
809
657
 
810
- function renderQueuePage(entries, filter, counts, unnotifiedCount = 0) {
658
+ function renderQueuePage(entries, filter, counts = 0) {
811
659
  const escapeHtml = (str) => {
812
660
  if (typeof str !== 'string') str = JSON.stringify(str, null, 2);
813
661
  return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
@@ -1129,7 +977,7 @@ function renderQueuePage(entries, filter, counts, unnotifiedCount = 0) {
1129
977
  <div class="filter-bar" id="filter-bar">
1130
978
  ${filterLinks}
1131
979
  <div class="clear-section">
1132
- ${unnotifiedCount > 0 ? `<button type="button" class="btn-sm btn-primary" onclick="retryAllNotifications()" id="retry-all-btn">Retry ${unnotifiedCount} Notification${unnotifiedCount > 1 ? 's' : ''}</button>` : ''}
980
+
1133
981
  ${filter === 'completed' && counts.completed > 0 ? '<button type="button" class="btn-sm btn-danger" onclick="clearByStatus(\'completed\')">Clear Completed</button>' : ''}
1134
982
  ${filter === 'failed' && counts.failed > 0 ? '<button type="button" class="btn-sm btn-danger" onclick="clearByStatus(\'failed\')">Clear Failed</button>' : ''}
1135
983
  ${filter === 'rejected' && counts.rejected > 0 ? '<button type="button" class="btn-sm btn-danger" onclick="clearByStatus(\'rejected\')">Clear Rejected</button>' : ''}
@@ -1,286 +0,0 @@
1
- import { getSetting, updateQueueNotification } from './db.js';
2
-
3
- /**
4
- * Send a notification to Clawdbot webhook when queue items complete/fail
5
- */
6
-
7
- const DEFAULT_RETRY_ATTEMPTS = 3;
8
- const DEFAULT_RETRY_DELAY_MS = 5000;
9
-
10
- /**
11
- * Find the most relevant result to display (usually last one with a URL, or first failure)
12
- */
13
- function findRelevantResult(results, forError = false) {
14
- if (!results?.length) return null;
15
-
16
- if (forError) {
17
- // For errors, find the first non-ok result
18
- return results.find(r => !r.ok) || results[results.length - 1];
19
- }
20
-
21
- // For success, find the last result with a useful URL (PR, issue, etc.)
22
- // Search backwards to get the most relevant one (e.g., PR URL, not branch ref)
23
- for (let i = results.length - 1; i >= 0; i--) {
24
- const r = results[i];
25
- if (r.body?.html_url || r.body?.url) {
26
- return r;
27
- }
28
- }
29
-
30
- // Fallback to last result
31
- return results[results.length - 1];
32
- }
33
-
34
- /**
35
- * Format the notification text for a queue entry
36
- */
37
- function formatNotification(entry) {
38
- const emoji = entry.status === 'completed' ? '✅' :
39
- entry.status === 'failed' ? '❌' : '🚫';
40
-
41
- let text = `${emoji} [agentgate] Queue #${entry.id} ${entry.status}`;
42
- text += `\n→ ${entry.service}/${entry.account_name}`;
43
-
44
- // Include key result info (e.g., PR URL, issue URL)
45
- if (entry.status === 'completed' && entry.results?.length) {
46
- const relevantResult = findRelevantResult(entry.results, false);
47
- if (relevantResult?.body) {
48
- // GitHub PR/Issue
49
- if (relevantResult.body.html_url) {
50
- text += `\n→ ${relevantResult.body.html_url}`;
51
- }
52
- // Other useful fields
53
- else if (relevantResult.body.url) {
54
- text += `\n→ ${relevantResult.body.url}`;
55
- }
56
- }
57
- }
58
-
59
- // Include error info for failures
60
- if (entry.status === 'failed' && entry.results?.length) {
61
- const failingResult = findRelevantResult(entry.results, true);
62
- if (failingResult.error) {
63
- text += `\n→ Error: ${failingResult.error}`;
64
- } else if (failingResult.body?.message) {
65
- text += `\n→ Error: ${failingResult.body.message}`;
66
- } else if (!failingResult.ok && failingResult.status) {
67
- text += `\n→ Error: HTTP ${failingResult.status}`;
68
- }
69
- }
70
-
71
- // Include rejection reason
72
- if (entry.status === 'rejected' && entry.rejection_reason) {
73
- text += `\n→ Reason: ${entry.rejection_reason}`;
74
- }
75
-
76
- // Original comment/intent
77
- if (entry.comment) {
78
- // Truncate long comments
79
- const comment = entry.comment.length > 100
80
- ? entry.comment.substring(0, 100) + '...'
81
- : entry.comment;
82
- text += `\nOriginal: "${comment}"`;
83
- }
84
-
85
- return text;
86
- }
87
-
88
- /**
89
- * Sleep helper
90
- */
91
- function sleep(ms) {
92
- return new Promise(resolve => setTimeout(resolve, ms));
93
- }
94
-
95
- /**
96
- * Send notification to Clawdbot
97
- * @param {object} entry - The queue entry to notify about
98
- * @returns {Promise<{success: boolean, error?: string}>}
99
- */
100
- export async function notifyClawdbot(entry) {
101
- const config = getSetting('notifications');
102
-
103
- if (!config?.clawdbot?.enabled) {
104
- return { success: true, skipped: true };
105
- }
106
-
107
- const { url, token, events, retryAttempts, retryDelayMs } = config.clawdbot;
108
-
109
- // Check if this event type should be notified
110
- const allowedEvents = events || ['completed', 'failed'];
111
- if (!allowedEvents.includes(entry.status)) {
112
- return { success: true, skipped: true };
113
- }
114
-
115
- if (!url || !token) {
116
- const error = 'Clawdbot notification config incomplete (missing url or token)';
117
- updateQueueNotification(entry.id, false, error);
118
- return { success: false, error };
119
- }
120
-
121
- const payload = {
122
- text: formatNotification(entry),
123
- mode: 'now'
124
- };
125
-
126
- const maxAttempts = retryAttempts || DEFAULT_RETRY_ATTEMPTS;
127
- const delay = retryDelayMs || DEFAULT_RETRY_DELAY_MS;
128
- let lastError = null;
129
-
130
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
131
- try {
132
- const response = await fetch(url, {
133
- method: 'POST',
134
- headers: {
135
- 'Authorization': `Bearer ${token}`,
136
- 'Content-Type': 'application/json'
137
- },
138
- body: JSON.stringify(payload)
139
- });
140
-
141
- if (response.ok) {
142
- // Success
143
- updateQueueNotification(entry.id, true, null);
144
- return { success: true };
145
- }
146
-
147
- // Non-OK response
148
- const text = await response.text().catch(() => '');
149
- lastError = `HTTP ${response.status}: ${text.substring(0, 100)}`;
150
-
151
- } catch (err) {
152
- lastError = err.message;
153
- }
154
-
155
- // Retry after delay (unless last attempt)
156
- if (attempt < maxAttempts) {
157
- await sleep(delay);
158
- }
159
- }
160
-
161
- // All retries failed
162
- updateQueueNotification(entry.id, false, lastError);
163
- return { success: false, error: lastError };
164
- }
165
-
166
- /**
167
- * Retry notification for a specific queue entry
168
- */
169
- export async function retryNotification(entryId, getQueueEntry) {
170
- const entry = getQueueEntry(entryId);
171
- if (!entry) {
172
- return { success: false, error: 'Queue entry not found' };
173
- }
174
-
175
- // Only retry for completed/failed/rejected entries
176
- if (!['completed', 'failed', 'rejected'].includes(entry.status)) {
177
- return { success: false, error: 'Can only notify for completed/failed/rejected entries' };
178
- }
179
-
180
- return notifyClawdbot(entry);
181
- }
182
-
183
- /**
184
- * Format a short summary line for batch notifications
185
- */
186
- function formatBatchLine(entry) {
187
- const emoji = entry.status === 'completed' ? '✅' :
188
- entry.status === 'failed' ? '❌' : '🚫';
189
-
190
- let line = `${emoji} #${entry.id.substring(0, 8)} - ${entry.service}/${entry.account_name}`;
191
-
192
- // Add brief result info
193
- if (entry.status === 'completed') {
194
- const relevantResult = findRelevantResult(entry.results, false);
195
- if (relevantResult?.body?.html_url) {
196
- line += ` - ${relevantResult.body.html_url}`;
197
- }
198
- } else if (entry.status === 'failed') {
199
- const failingResult = findRelevantResult(entry.results, true);
200
- const err = failingResult?.error || failingResult?.body?.message || (failingResult && !failingResult.ok ? `HTTP ${failingResult.status || '?'}` : 'Unknown error');
201
- line += ` - ${err.substring(0, 50)}`;
202
- } else if (entry.status === 'rejected') {
203
- line += ` - ${(entry.rejection_reason || 'rejected').substring(0, 50)}`;
204
- }
205
-
206
- return line;
207
- }
208
-
209
- /**
210
- * Send a single batched notification for multiple queue entries
211
- * @param {object[]} entries - Array of queue entries to notify about
212
- * @returns {Promise<{success: boolean, error?: string}>}
213
- */
214
- export async function notifyClawdbotBatch(entries) {
215
- const config = getSetting('notifications');
216
-
217
- if (!config?.clawdbot?.enabled) {
218
- return { success: true, skipped: true };
219
- }
220
-
221
- const { url, token, retryAttempts, retryDelayMs } = config.clawdbot;
222
-
223
- if (!url || !token) {
224
- const error = 'Clawdbot notification config incomplete (missing url or token)';
225
- for (const entry of entries) {
226
- updateQueueNotification(entry.id, false, error);
227
- }
228
- return { success: false, error };
229
- }
230
-
231
- // Build batched message
232
- const completed = entries.filter(e => e.status === 'completed').length;
233
- const failed = entries.filter(e => e.status === 'failed').length;
234
- const rejected = entries.filter(e => e.status === 'rejected').length;
235
-
236
- const counts = [];
237
- if (completed) counts.push(`${completed} completed`);
238
- if (failed) counts.push(`${failed} failed`);
239
- if (rejected) counts.push(`${rejected} rejected`);
240
-
241
- let text = `📬 [agentgate] Catch-up: ${entries.length} item${entries.length > 1 ? 's' : ''} (${counts.join(', ')})\n\n`;
242
- text += entries.map(formatBatchLine).join('\n');
243
-
244
- const payload = { text, mode: 'now' };
245
-
246
- const maxAttempts = retryAttempts || DEFAULT_RETRY_ATTEMPTS;
247
- const delay = retryDelayMs || DEFAULT_RETRY_DELAY_MS;
248
- let lastError = null;
249
-
250
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
251
- try {
252
- const response = await fetch(url, {
253
- method: 'POST',
254
- headers: {
255
- 'Authorization': `Bearer ${token}`,
256
- 'Content-Type': 'application/json'
257
- },
258
- body: JSON.stringify(payload)
259
- });
260
-
261
- if (response.ok) {
262
- // Success - mark all as notified
263
- for (const entry of entries) {
264
- updateQueueNotification(entry.id, true, null);
265
- }
266
- return { success: true };
267
- }
268
-
269
- const respText = await response.text().catch(() => '');
270
- lastError = `HTTP ${response.status}: ${respText.substring(0, 100)}`;
271
-
272
- } catch (err) {
273
- lastError = err.message;
274
- }
275
-
276
- if (attempt < maxAttempts) {
277
- await sleep(delay);
278
- }
279
- }
280
-
281
- // All retries failed
282
- for (const entry of entries) {
283
- updateQueueNotification(entry.id, false, lastError);
284
- }
285
- return { success: false, error: lastError };
286
- }