aoaoe 0.58.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 +4 -0
- package/dist/config.js +51 -18
- package/dist/health.d.ts +26 -0
- package/dist/health.js +79 -0
- package/dist/index.js +14 -1
- package/dist/notify.d.ts +1 -0
- package/dist/notify.js +33 -8
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -259,6 +259,7 @@ options:
|
|
|
259
259
|
--reasoner <opencode|claude-code> reasoning backend (default: opencode)
|
|
260
260
|
--poll-interval <ms> poll interval in ms (default: 10000)
|
|
261
261
|
--port <number> opencode server port (default: 4097)
|
|
262
|
+
--health-port <number> start HTTP health check server on this port
|
|
262
263
|
--model <model> model to use
|
|
263
264
|
--profile <name> aoe profile (default: default)
|
|
264
265
|
--dry-run run full loop but only log actions (costs
|
|
@@ -336,9 +337,11 @@ Config lives at `~/.aoaoe/aoaoe.config.json` (canonical, written by `aoaoe init`
|
|
|
336
337
|
| `sessionDirs` | Map session titles to project directories (relative to cwd or absolute). Bypasses heuristic directory search. | `{}` |
|
|
337
338
|
| `contextFiles` | Extra AI instruction file paths to load from each project root | `[]` |
|
|
338
339
|
| `captureLinesCount` | Number of tmux lines to capture per session (`-S` flag) | `100` |
|
|
340
|
+
| `healthPort` | Start HTTP health check server on this port (e.g. `4098`). GET `/health` returns JSON status. | (none) |
|
|
339
341
|
| `notifications.webhookUrl` | Generic webhook URL (POST JSON) | (none) |
|
|
340
342
|
| `notifications.slackWebhookUrl` | Slack incoming webhook URL (block kit format) | (none) |
|
|
341
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) |
|
|
342
345
|
|
|
343
346
|
Also reads `.aoaoe.json` as an alternative config filename.
|
|
344
347
|
|
|
@@ -497,6 +500,7 @@ src/
|
|
|
497
500
|
input.ts # stdin readline listener with inject() for post-interrupt
|
|
498
501
|
init.ts # `aoaoe init`: auto-discover tools, sessions, generate config
|
|
499
502
|
notify.ts # webhook + Slack notification dispatcher for daemon events
|
|
503
|
+
health.ts # HTTP health check endpoint (GET /health JSON status)
|
|
500
504
|
colors.ts # shared ANSI color/style constants
|
|
501
505
|
context.ts # discoverContextFiles, resolveProjectDir, loadSessionContext
|
|
502
506
|
activity.ts # detect human keystrokes in tmux sessions
|
package/dist/config.js
CHANGED
|
@@ -83,7 +83,7 @@ export function loadConfig(overrides) {
|
|
|
83
83
|
const KNOWN_KEYS = {
|
|
84
84
|
reasoner: true, pollIntervalMs: true, captureLinesCount: true,
|
|
85
85
|
verbose: true, dryRun: true, observe: true, confirm: true,
|
|
86
|
-
contextFiles: true, sessionDirs: true, protectedSessions: true,
|
|
86
|
+
contextFiles: true, sessionDirs: true, protectedSessions: true, healthPort: true,
|
|
87
87
|
opencode: new Set(["port", "model"]),
|
|
88
88
|
claudeCode: new Set(["model", "yolo", "resume"]),
|
|
89
89
|
aoe: new Set(["profile"]),
|
|
@@ -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))
|
|
@@ -128,6 +128,11 @@ export function validateConfig(config) {
|
|
|
128
128
|
if (typeof config.opencode?.port !== "number" || !isFinite(config.opencode.port) || config.opencode.port < 1 || config.opencode.port > 65535) {
|
|
129
129
|
errors.push(`opencode.port must be 1-65535, got ${config.opencode?.port}`);
|
|
130
130
|
}
|
|
131
|
+
if (config.healthPort !== undefined) {
|
|
132
|
+
if (typeof config.healthPort !== "number" || !isFinite(config.healthPort) || config.healthPort < 1 || config.healthPort > 65535) {
|
|
133
|
+
errors.push(`healthPort must be 1-65535, got ${config.healthPort}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
131
136
|
if (typeof config.policies?.maxErrorsBeforeRestart !== "number" || config.policies.maxErrorsBeforeRestart < 1) {
|
|
132
137
|
errors.push(`policies.maxErrorsBeforeRestart must be >= 1, got ${config.policies?.maxErrorsBeforeRestart}`);
|
|
133
138
|
}
|
|
@@ -211,6 +216,13 @@ export function validateConfig(config) {
|
|
|
211
216
|
}
|
|
212
217
|
}
|
|
213
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
|
+
}
|
|
214
226
|
if (errors.length > 0) {
|
|
215
227
|
throw new Error(`invalid config:\n ${errors.join("\n ")}`);
|
|
216
228
|
}
|
|
@@ -241,22 +253,31 @@ async function which(cmd) {
|
|
|
241
253
|
return false;
|
|
242
254
|
}
|
|
243
255
|
}
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
// internal recursive merge on plain Record objects (no type assertions needed)
|
|
257
|
+
function mergeRecords(target, source) {
|
|
258
|
+
for (const [key, val] of Object.entries(source)) {
|
|
259
|
+
if (val !== undefined && val !== null) {
|
|
260
|
+
const existing = target[key];
|
|
261
|
+
// empty objects ({}) replace rather than merge — allows clearing sessionDirs etc.
|
|
262
|
+
if (typeof val === "object" && !Array.isArray(val) &&
|
|
263
|
+
typeof existing === "object" && existing !== null && !Array.isArray(existing) &&
|
|
264
|
+
Object.keys(val).length > 0) {
|
|
265
|
+
target[key] = mergeRecords({ ...existing }, val);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
target[key] = val;
|
|
257
269
|
}
|
|
258
270
|
}
|
|
259
271
|
}
|
|
272
|
+
return target;
|
|
273
|
+
}
|
|
274
|
+
// exported for testing — merges config objects with nested object support
|
|
275
|
+
export function deepMerge(...objects) {
|
|
276
|
+
let result = {};
|
|
277
|
+
for (const obj of objects) {
|
|
278
|
+
result = mergeRecords(result, obj);
|
|
279
|
+
}
|
|
280
|
+
// validated by caller (validateConfig) before use — safe cast
|
|
260
281
|
return result;
|
|
261
282
|
}
|
|
262
283
|
// compute fields that differ between two config objects (flat dot-notation paths)
|
|
@@ -368,7 +389,7 @@ export function parseCliArgs(argv) {
|
|
|
368
389
|
return argv[i + 1];
|
|
369
390
|
};
|
|
370
391
|
const knownFlags = new Set([
|
|
371
|
-
"--reasoner", "--poll-interval", "--port", "--model", "--profile",
|
|
392
|
+
"--reasoner", "--poll-interval", "--port", "--model", "--profile", "--health-port",
|
|
372
393
|
"--verbose", "-v", "--dry-run", "--observe", "--confirm", "--help", "-h", "--version",
|
|
373
394
|
]);
|
|
374
395
|
for (let i = 2; i < argv.length; i++) {
|
|
@@ -394,6 +415,14 @@ export function parseCliArgs(argv) {
|
|
|
394
415
|
i++;
|
|
395
416
|
break;
|
|
396
417
|
}
|
|
418
|
+
case "--health-port": {
|
|
419
|
+
const val = parseInt(nextArg(i, arg), 10);
|
|
420
|
+
if (isNaN(val))
|
|
421
|
+
throw new Error(`--health-port value '${argv[i + 1]}' is not a valid number`);
|
|
422
|
+
overrides.healthPort = val;
|
|
423
|
+
i++;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
397
426
|
case "--model": {
|
|
398
427
|
// applies to whichever backend is selected
|
|
399
428
|
const model = nextArg(i, arg);
|
|
@@ -472,6 +501,7 @@ options:
|
|
|
472
501
|
--reasoner <opencode|claude-code> reasoning backend (default: opencode)
|
|
473
502
|
--poll-interval <ms> poll interval in ms (default: 10000)
|
|
474
503
|
--port <number> opencode server port (default: 4097)
|
|
504
|
+
--health-port <number> start HTTP health check server on this port
|
|
475
505
|
--model <model> model to use
|
|
476
506
|
--profile <name> aoe profile (default: default)
|
|
477
507
|
--dry-run run full loop but only log actions (costs
|
|
@@ -509,10 +539,12 @@ example config:
|
|
|
509
539
|
"my-project": "/path/to/my-project",
|
|
510
540
|
"other-repo": "/path/to/other-repo"
|
|
511
541
|
},
|
|
542
|
+
"healthPort": 4098,
|
|
512
543
|
"notifications": {
|
|
513
544
|
"webhookUrl": "https://example.com/webhook",
|
|
514
545
|
"slackWebhookUrl": "https://hooks.slack.com/services/T.../B.../xxx",
|
|
515
|
-
"events": ["session_error", "session_done", "daemon_started", "daemon_stopped"]
|
|
546
|
+
"events": ["session_error", "session_done", "daemon_started", "daemon_stopped"],
|
|
547
|
+
"maxRetries": 2
|
|
516
548
|
}
|
|
517
549
|
}
|
|
518
550
|
|
|
@@ -522,7 +554,8 @@ example config:
|
|
|
522
554
|
|
|
523
555
|
notifications sends webhook alerts for daemon events. Both webhookUrl
|
|
524
556
|
and slackWebhookUrl are optional. events filters which events fire
|
|
525
|
-
(omit to send all).
|
|
557
|
+
(omit to send all). maxRetries enables exponential backoff retry on
|
|
558
|
+
failure (default: 0 = no retry). Run 'aoaoe notify-test' to verify.
|
|
526
559
|
|
|
527
560
|
interactive commands (while daemon is running):
|
|
528
561
|
/help show available commands
|
package/dist/health.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
import type { DaemonState } from "./types.js";
|
|
3
|
+
export declare function buildHealthResponse(state: DaemonState | null, startedAt: number, now?: number): HealthResponse;
|
|
4
|
+
export interface HealthResponse {
|
|
5
|
+
status: "ok" | "error";
|
|
6
|
+
version: string;
|
|
7
|
+
uptimeMs: number;
|
|
8
|
+
daemon: {
|
|
9
|
+
phase: string;
|
|
10
|
+
phaseStartedAt: number;
|
|
11
|
+
pollCount: number;
|
|
12
|
+
pollIntervalMs: number;
|
|
13
|
+
sessionCount: number;
|
|
14
|
+
changeCount: number;
|
|
15
|
+
paused: boolean;
|
|
16
|
+
sessions: Array<{
|
|
17
|
+
title: string;
|
|
18
|
+
tool: string;
|
|
19
|
+
status: string;
|
|
20
|
+
currentTask?: string;
|
|
21
|
+
userActive: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
} | null;
|
|
24
|
+
}
|
|
25
|
+
export declare function startHealthServer(port: number, startedAt: number): Server;
|
|
26
|
+
//# sourceMappingURL=health.d.ts.map
|
package/dist/health.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// health.ts — HTTP health check endpoint for daemon monitoring
|
|
2
|
+
// starts a lightweight HTTP server that responds to GET /health with JSON status.
|
|
3
|
+
// enabled when config.healthPort is set (opt-in). the server reads daemon state
|
|
4
|
+
// from the IPC state file and returns uptime, phase, session info, and version.
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { readState } from "./daemon-state.js";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join, dirname } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
// read version from package.json at startup (cached)
|
|
11
|
+
let cachedVersion;
|
|
12
|
+
function getVersion() {
|
|
13
|
+
if (cachedVersion)
|
|
14
|
+
return cachedVersion;
|
|
15
|
+
try {
|
|
16
|
+
// resolve from compiled dist/ to project root package.json
|
|
17
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const pkg = JSON.parse(readFileSync(join(thisDir, "..", "package.json"), "utf-8"));
|
|
19
|
+
cachedVersion = pkg.version ?? "unknown";
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
cachedVersion = "unknown";
|
|
23
|
+
}
|
|
24
|
+
return cachedVersion;
|
|
25
|
+
}
|
|
26
|
+
// pure function: build health response JSON from daemon state (exported for testing)
|
|
27
|
+
export function buildHealthResponse(state, startedAt, now = Date.now()) {
|
|
28
|
+
const uptimeMs = now - startedAt;
|
|
29
|
+
const version = getVersion();
|
|
30
|
+
if (!state) {
|
|
31
|
+
return {
|
|
32
|
+
status: "error",
|
|
33
|
+
version,
|
|
34
|
+
uptimeMs,
|
|
35
|
+
daemon: null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
status: "ok",
|
|
40
|
+
version,
|
|
41
|
+
uptimeMs,
|
|
42
|
+
daemon: {
|
|
43
|
+
phase: state.phase,
|
|
44
|
+
phaseStartedAt: state.phaseStartedAt,
|
|
45
|
+
pollCount: state.pollCount,
|
|
46
|
+
pollIntervalMs: state.pollIntervalMs,
|
|
47
|
+
sessionCount: state.sessionCount,
|
|
48
|
+
changeCount: state.changeCount,
|
|
49
|
+
paused: state.paused,
|
|
50
|
+
sessions: state.sessions.map((s) => ({
|
|
51
|
+
title: s.title,
|
|
52
|
+
tool: s.tool,
|
|
53
|
+
status: s.status,
|
|
54
|
+
currentTask: s.currentTask,
|
|
55
|
+
userActive: s.userActive ?? false,
|
|
56
|
+
})),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// start the health HTTP server on the given port. returns the server for shutdown.
|
|
61
|
+
export function startHealthServer(port, startedAt) {
|
|
62
|
+
const server = createServer((req, res) => {
|
|
63
|
+
// only respond to GET /health (and GET / as convenience alias)
|
|
64
|
+
if (req.method === "GET" && (req.url === "/health" || req.url === "/")) {
|
|
65
|
+
const state = readState();
|
|
66
|
+
const body = JSON.stringify(buildHealthResponse(state, startedAt), null, 2);
|
|
67
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" });
|
|
68
|
+
res.end(body + "\n");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
72
|
+
res.end(JSON.stringify({ error: "not found", hint: "try GET /health" }) + "\n");
|
|
73
|
+
});
|
|
74
|
+
server.listen(port, "127.0.0.1", () => {
|
|
75
|
+
// listening — logged by caller
|
|
76
|
+
});
|
|
77
|
+
return server;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=health.js.map
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { runTaskCli, handleTaskSlashCommand } from "./task-cli.js";
|
|
|
18
18
|
import { TUI } from "./tui.js";
|
|
19
19
|
import { isDaemonRunningFromState } from "./chat.js";
|
|
20
20
|
import { sendNotification, sendTestNotification } from "./notify.js";
|
|
21
|
+
import { startHealthServer } from "./health.js";
|
|
21
22
|
import { actionSession, actionDetail, toActionLogEntry } from "./types.js";
|
|
22
23
|
import { YELLOW, GREEN, DIM, BOLD, RED, RESET } from "./colors.js";
|
|
23
24
|
import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
@@ -266,8 +267,18 @@ async function main() {
|
|
|
266
267
|
}
|
|
267
268
|
catch { }
|
|
268
269
|
}
|
|
269
|
-
// ──
|
|
270
|
+
// ── health check HTTP server (opt-in via config.healthPort) ────────────────
|
|
270
271
|
const daemonStartedAt = Date.now();
|
|
272
|
+
let healthServer = null;
|
|
273
|
+
if (config.healthPort) {
|
|
274
|
+
healthServer = startHealthServer(config.healthPort, daemonStartedAt);
|
|
275
|
+
const msg = `health server listening on http://127.0.0.1:${config.healthPort}/health`;
|
|
276
|
+
if (tui)
|
|
277
|
+
tui.log("system", msg);
|
|
278
|
+
else
|
|
279
|
+
log(msg);
|
|
280
|
+
}
|
|
281
|
+
// ── session stats (for shutdown summary) ──────────────────────────────────
|
|
271
282
|
let totalDecisions = 0;
|
|
272
283
|
let totalActionsExecuted = 0;
|
|
273
284
|
let totalActionsFailed = 0;
|
|
@@ -308,6 +319,8 @@ async function main() {
|
|
|
308
319
|
console.error(` mode: dry-run (no execution)`);
|
|
309
320
|
console.error("");
|
|
310
321
|
log("shutting down...");
|
|
322
|
+
if (healthServer)
|
|
323
|
+
healthServer.close();
|
|
311
324
|
// notify: daemon stopped (fire-and-forget, don't block shutdown)
|
|
312
325
|
sendNotification(config, { event: "daemon_stopped", timestamp: Date.now(), detail: `polls: ${totalPolls}, actions: ${totalActionsExecuted}` });
|
|
313
326
|
input.stop();
|
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
|
|
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
|
|
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,7 +119,9 @@ export interface AoaoeConfig {
|
|
|
119
119
|
webhookUrl?: string;
|
|
120
120
|
slackWebhookUrl?: string;
|
|
121
121
|
events?: NotificationEvent[];
|
|
122
|
+
maxRetries?: number;
|
|
122
123
|
};
|
|
124
|
+
healthPort?: number;
|
|
123
125
|
}
|
|
124
126
|
export type NotificationEvent = "session_error" | "session_done" | "action_executed" | "action_failed" | "daemon_started" | "daemon_stopped";
|
|
125
127
|
export type DaemonPhase = "sleeping" | "polling" | "reasoning" | "executing" | "interrupted";
|