aoaoe 0.59.0 → 0.60.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/README.md CHANGED
@@ -341,6 +341,7 @@ Config lives at `~/.aoaoe/aoaoe.config.json` (canonical, written by `aoaoe init`
341
341
  | `notifications.webhookUrl` | Generic webhook URL (POST JSON) | (none) |
342
342
  | `notifications.slackWebhookUrl` | Slack incoming webhook URL (block kit format) | (none) |
343
343
  | `notifications.events` | Filter which events fire (omit to send all). Valid: `session_error`, `session_done`, `action_executed`, `action_failed`, `daemon_started`, `daemon_stopped` | (all) |
344
+ | `notifications.maxRetries` | Retry failed webhook deliveries with exponential backoff (1s, 2s, 4s, ...) | `0` (no retry) |
344
345
 
345
346
  Also reads `.aoaoe.json` as an alternative config filename.
346
347
 
package/dist/config.js CHANGED
@@ -91,7 +91,7 @@ const KNOWN_KEYS = {
91
91
  "maxIdleBeforeNudgeMs", "maxErrorsBeforeRestart", "autoAnswerPermissions",
92
92
  "actionCooldownMs", "userActivityThresholdMs", "allowDestructive",
93
93
  ]),
94
- notifications: new Set(["webhookUrl", "slackWebhookUrl", "events"]),
94
+ notifications: new Set(["webhookUrl", "slackWebhookUrl", "events", "maxRetries"]),
95
95
  };
96
96
  export function warnUnknownKeys(raw, source) {
97
97
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
@@ -216,6 +216,13 @@ export function validateConfig(config) {
216
216
  }
217
217
  }
218
218
  }
219
+ // notifications.maxRetries must be a non-negative integer
220
+ if (config.notifications?.maxRetries !== undefined) {
221
+ const r = config.notifications.maxRetries;
222
+ if (typeof r !== "number" || !isFinite(r) || r < 0 || !Number.isInteger(r)) {
223
+ errors.push(`notifications.maxRetries must be a non-negative integer, got ${r}`);
224
+ }
225
+ }
219
226
  if (errors.length > 0) {
220
227
  throw new Error(`invalid config:\n ${errors.join("\n ")}`);
221
228
  }
@@ -536,7 +543,8 @@ example config:
536
543
  "notifications": {
537
544
  "webhookUrl": "https://example.com/webhook",
538
545
  "slackWebhookUrl": "https://hooks.slack.com/services/T.../B.../xxx",
539
- "events": ["session_error", "session_done", "daemon_started", "daemon_stopped"]
546
+ "events": ["session_error", "session_done", "daemon_started", "daemon_stopped"],
547
+ "maxRetries": 2
540
548
  }
541
549
  }
542
550
 
@@ -546,7 +554,8 @@ example config:
546
554
 
547
555
  notifications sends webhook alerts for daemon events. Both webhookUrl
548
556
  and slackWebhookUrl are optional. events filters which events fire
549
- (omit to send all). Run 'aoaoe notify-test' to verify delivery.
557
+ (omit to send all). maxRetries enables exponential backoff retry on
558
+ failure (default: 0 = no retry). Run 'aoaoe notify-test' to verify.
550
559
 
551
560
  interactive commands (while daemon is running):
552
561
  /help show available commands
package/dist/notify.d.ts CHANGED
@@ -14,6 +14,7 @@ export declare function sendTestNotification(config: AoaoeConfig): Promise<{
14
14
  webhookError?: string;
15
15
  slackError?: string;
16
16
  }>;
17
+ export declare function fetchWithRetry(url: string, options: RequestInit, maxRetries?: number, baseDelayMs?: number): Promise<Response>;
17
18
  export declare function formatSlackPayload(payload: NotificationPayload): {
18
19
  text: string;
19
20
  blocks: object[];
package/dist/notify.js CHANGED
@@ -45,12 +45,13 @@ export async function sendNotification(config, payload) {
45
45
  if (isRateLimited(payload))
46
46
  return;
47
47
  recordSent(payload);
48
+ const retries = n.maxRetries ?? 0;
48
49
  const promises = [];
49
50
  if (n.webhookUrl) {
50
- promises.push(sendGenericWebhook(n.webhookUrl, payload));
51
+ promises.push(sendGenericWebhook(n.webhookUrl, payload, retries));
51
52
  }
52
53
  if (n.slackWebhookUrl) {
53
- promises.push(sendSlackWebhook(n.slackWebhookUrl, payload));
54
+ promises.push(sendSlackWebhook(n.slackWebhookUrl, payload, retries));
54
55
  }
55
56
  // fire-and-forget — swallow all errors so the daemon never crashes on notification failure
56
57
  await Promise.allSettled(promises);
@@ -104,10 +105,34 @@ export async function sendTestNotification(config) {
104
105
  }
105
106
  return result;
106
107
  }
108
+ // ── retry with exponential backoff ──────────────────────────────────────────
109
+ // exported for testing. retries failed fetch calls with exponential backoff.
110
+ // maxRetries=0 means no retry (single attempt). delay doubles each attempt.
111
+ export async function fetchWithRetry(url, options, maxRetries = 0, baseDelayMs = 1000) {
112
+ let lastError;
113
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
114
+ try {
115
+ const resp = await fetch(url, options);
116
+ if (resp.ok || attempt === maxRetries)
117
+ return resp;
118
+ // non-ok response: retry if we have attempts left
119
+ lastError = new Error(`HTTP ${resp.status} ${resp.statusText}`);
120
+ }
121
+ catch (err) {
122
+ lastError = err;
123
+ if (attempt === maxRetries)
124
+ break;
125
+ }
126
+ // exponential backoff: baseDelay * 2^attempt (1s, 2s, 4s, ...)
127
+ const delay = baseDelayMs * Math.pow(2, attempt);
128
+ await new Promise((r) => setTimeout(r, delay));
129
+ }
130
+ throw lastError;
131
+ }
107
132
  // POST JSON payload to a generic webhook URL
108
- async function sendGenericWebhook(url, payload) {
133
+ async function sendGenericWebhook(url, payload, maxRetries = 0) {
109
134
  try {
110
- await fetch(url, {
135
+ await fetchWithRetry(url, {
111
136
  method: "POST",
112
137
  headers: { "Content-Type": "application/json" },
113
138
  body: JSON.stringify({
@@ -117,22 +142,22 @@ async function sendGenericWebhook(url, payload) {
117
142
  detail: payload.detail,
118
143
  }),
119
144
  signal: AbortSignal.timeout(5000),
120
- });
145
+ }, maxRetries);
121
146
  }
122
147
  catch (err) {
123
148
  console.error(`[notify] generic webhook failed: ${err}`);
124
149
  }
125
150
  }
126
151
  // POST Slack block format to a Slack incoming webhook URL
127
- async function sendSlackWebhook(url, payload) {
152
+ async function sendSlackWebhook(url, payload, maxRetries = 0) {
128
153
  try {
129
154
  const body = formatSlackPayload(payload);
130
- await fetch(url, {
155
+ await fetchWithRetry(url, {
131
156
  method: "POST",
132
157
  headers: { "Content-Type": "application/json" },
133
158
  body: JSON.stringify(body),
134
159
  signal: AbortSignal.timeout(5000),
135
- });
160
+ }, maxRetries);
136
161
  }
137
162
  catch (err) {
138
163
  console.error(`[notify] slack webhook failed: ${err}`);
package/dist/types.d.ts CHANGED
@@ -119,6 +119,7 @@ export interface AoaoeConfig {
119
119
  webhookUrl?: string;
120
120
  slackWebhookUrl?: string;
121
121
  events?: NotificationEvent[];
122
+ maxRetries?: number;
122
123
  };
123
124
  healthPort?: number;
124
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.59.0",
3
+ "version": "0.60.0",
4
4
  "description": "Autonomous supervisor for agent-of-empires sessions using OpenCode or Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",