feat: add agent API and operational analytics

This commit is contained in:
jjbrunton committed 2026-08-08 22:08:14 +01:00
1 parent 720ea0652a
commit e8b730b592
65 files changed
+2650 -1214

No files matched your search

+73 -2
View File
@@ -291,6 +291,14 @@ export async function runMainLoop(app: AppContainer): Promise<void> {
await rateLimiter.throttle(
async () => {
if (config.browser.dryRun) {
const reason = 'Dry run: message prepared but not sent';
logger.info(`${reason} for u/${post.author}`);
postsRepo.setError(post.id, reason);
webServer?.logActivity('skipped', `${reason} to u/${post.author}`, post.author);
webServer?.pushLog('info', `🧪 ${reason} to u/${post.author}`);
return false;
}
if (!recipientDeduplicator.tryReserve(post.author)) {
const reason = 'User already contacted across accounts';
logger.info(`Skipping u/${post.author} - ${reason.toLowerCase()}`);
@@ -335,6 +343,11 @@ export async function runMainLoop(app: AppContainer): Promise<void> {
sentAt: Date.now(),
strategyId: processed.strategyId,
chatRoomId: chat.roomId,
experimentId: processed.experimentId,
promptVersion: processed.promptVersion,
modelId: processed.modelId,
selectionReason: processed.selectionReason,
sourceType: processed.sourceType,
});
const currentCount = currentHourlySendCount(rateLimiter);
@@ -410,14 +423,72 @@ export async function runMainLoop(app: AppContainer): Promise<void> {
}
async function runConcurrentAccountRuntimes(app: AppContainer): Promise<void> {
const { logger, accountRuntimeManager, browserClient, db, webServer } = app;
logger.info(`Running ${accountRuntimeManager.list().length} independent Reddit account runtime(s)`);
const { config, logger, accountRuntimeManager, browserClient, db, webServer } = app;
logger.info(`Running ${accountRuntimeManager.list().length} account runtime(s) with shared post discovery`);
await new Promise<void>((resolve) => {
let shuttingDown = false;
const controller = new AbortController();
let nextScannerIndex = 0;
const discoverAndDispatch = async (): Promise<void> => {
const runtimes = accountRuntimeManager.list()
.filter((snapshot) => snapshot.state === 'running')
.map((snapshot) => accountRuntimeManager.get(snapshot.accountId))
.filter((runtime): runtime is NonNullable<typeof runtime> => runtime !== null);
if (runtimes.length === 0) return;
// Rotate the authenticated browser used to read Reddit, while keeping
// selection and delivery centralised. A subreddit is therefore fetched
// once per cycle, never once per account.
const scanner = runtimes[nextScannerIndex % runtimes.length];
nextScannerIndex += 1;
if (!(await scanner.canAcceptPost())) return;
const posts = await scanner.resources.monitorService.getUnprocessedPosts();
const post = posts[0];
if (!post) return;
const recipients = (await Promise.all(runtimes.map(async (runtime) => ({
runtime,
available: await runtime.canAcceptPost(),
})))).filter((candidate) => candidate.available)
.map((candidate) => candidate.runtime)
.sort((left, right) => (
left.resources.rateLimiter.getStats().messagesThisHour
- right.resources.rateLimiter.getStats().messagesThisHour
));
const recipient = recipients[0];
if (!recipient) {
logger.debug(`Eligible post ${post.id} found, but no account is currently available`);
return;
}
logger.info(`Assigning eligible post ${post.id} (r/${post.subreddit}) to u/${recipient.session.redditUsername}`);
await recipient.dispatchPost(post, controller.signal);
};
const discoveryLoop = (async (): Promise<void> => {
while (!controller.signal.aborted) {
try {
if (webServer?.isRunning() ?? true) await discoverAndDispatch();
} catch (error) {
if (controller.signal.aborted) return;
if (error instanceof RedditRateLimitError) {
await sleep(error.retryAfterMs + 1_000, controller.signal).catch(() => undefined);
continue;
}
logger.error('Shared post discovery failed:', error);
}
await sleep(config.reddit.pollIntervalMs, controller.signal).catch(() => undefined);
}
})();
const shutdown = async (): Promise<void> => {
if (shuttingDown) return;
shuttingDown = true;
controller.abort();
logger.info('Stopping all Reddit account runtimes...');
await discoveryLoop;
await accountRuntimeManager.stopAll();
webServer?.stop();
await browserClient.close();