feat: flag accounts Reddit considers unestablished and re-test them
New accounts cannot start chats until Reddit considers them established. Until now the bot would burn a candidate on every invitation Reddit was always going to reject, with nothing in the dashboard explaining why the account looked idle. Two signals feed the verdict. A periodic profile check reads the account's own age, karma and email verification from about.json and compares them against configurable thresholds — that is what flags a brand new account before it wastes anything. A rejected invitation is authoritative: Reddit has told us directly, so the account is held out of sending with a backoff that doubles per rejection, and a delivered invitation clears the flag outright. An unestablished account takes no new posts (canAcceptPost), raises an attention item, reports an `unestablished` sending state, and shows its age/karma evidence on the Accounts page. An unchecked account is always allowed through, so a first run is never blocked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c43e534cf1
commit
89eb4583f8
25 files changed
+1425
-11
No files matched your search
@@ -15,6 +15,7 @@ import {
|
||||
} from '../database/repositories.js';
|
||||
import {
|
||||
AccountContentRepository,
|
||||
RedditAccountEstablishmentRepository,
|
||||
RedditAccountsRepository,
|
||||
RedditRecipientAccountStatusRepository,
|
||||
} from '../database/account-repositories.js';
|
||||
@@ -53,6 +54,7 @@ import { AiUsageRepository } from '../database/ai-usage-repository.js';
|
||||
import { MediaLibraryRepository } from '../database/media-library-repository.js';
|
||||
import { MediaLibraryExchangeService } from './media-library-exchange.js';
|
||||
import { RecipientAccountStatusService } from './recipient-account-status.js';
|
||||
import { AccountEstablishmentService } from './account-establishment.js';
|
||||
|
||||
export interface ConcreteRuntimeFactoryOptions {
|
||||
config: Config;
|
||||
@@ -225,6 +227,15 @@ async function buildRuntime(
|
||||
logger,
|
||||
accountAuth,
|
||||
);
|
||||
const establishmentService = new AccountEstablishmentService(
|
||||
session.accountId,
|
||||
session.redditUsername,
|
||||
redditApi,
|
||||
new RedditAccountEstablishmentRepository(db),
|
||||
accountConfig.accountEstablishment,
|
||||
logger,
|
||||
accountAuth,
|
||||
);
|
||||
const responseChecker = new ResponseCheckerService(
|
||||
chatPage, messagesRepo, successfulMessagesRepo, postsRepo, logger,
|
||||
accountAuth, conversationOutcomesRepo,
|
||||
@@ -277,7 +288,7 @@ async function buildRuntime(
|
||||
activityLogRepo,
|
||||
monitorService, processorService, responseChecker, conversationAutomation, chatService, mediaService,
|
||||
mediaSyncService, mediaExchange, rateLimiter,
|
||||
recipientAccountStatusService,
|
||||
recipientAccountStatusService, establishmentService,
|
||||
};
|
||||
const lifecycle = new AccountRuntimeLoop(
|
||||
resources, accountConfig, logger, isBotRunning, recipientDeduplicator,
|
||||
@@ -318,6 +329,7 @@ class AccountRuntimeLoop {
|
||||
this.controller = new AbortController();
|
||||
this.resources.mediaSyncService.start();
|
||||
this.resources.recipientAccountStatusService.start();
|
||||
this.resources.establishmentService.start();
|
||||
void this.resources.chatService.runStartupSync()
|
||||
.then(() => this.resources.chatService.enrichUsernames())
|
||||
.catch((error) => this.logger.error(`[account ${this.resources.accountAuth.getSnapshot().redditUsername}] startup sync failed:`, error));
|
||||
@@ -328,6 +340,7 @@ class AccountRuntimeLoop {
|
||||
this.controller?.abort();
|
||||
this.resources.mediaSyncService.stop();
|
||||
this.resources.recipientAccountStatusService.stop();
|
||||
this.resources.establishmentService.stop();
|
||||
await this.loop?.catch((error) => {
|
||||
if (!(error instanceof DOMException && error.name === 'AbortError')) throw error;
|
||||
});
|
||||
@@ -415,7 +428,7 @@ class AccountRuntimeLoop {
|
||||
}
|
||||
|
||||
async dispatchPreparedPost(processed: ProcessedPost, signal?: AbortSignal): Promise<PreparedDeliveryResult> {
|
||||
const { postsRepo, messagesRepo, redditApi, rateLimiter, chatService } = this.resources;
|
||||
const { postsRepo, messagesRepo, redditApi, rateLimiter, chatService, establishmentService } = this.resources;
|
||||
const { post } = processed;
|
||||
if (signal?.aborted) {
|
||||
return { status: 'retryable', reason: 'Delivery interrupted by shutdown', retryAt: Date.now() + 5_000 };
|
||||
@@ -446,6 +459,13 @@ class AccountRuntimeLoop {
|
||||
);
|
||||
if (start.status !== 'sent') {
|
||||
this.recipientDeduplicator.release(post.author);
|
||||
// The rejection reason is the only place Reddit tells us the account is
|
||||
// too new to invite anyone; the code, when there is one, rides on the
|
||||
// cooldown the chat service just recorded.
|
||||
establishmentService.recordSendFailure(
|
||||
start.reason,
|
||||
chatService.getInvitationCooldown()?.redditCode ?? null,
|
||||
);
|
||||
if (start.status === 'terminal') {
|
||||
postsRepo.setError(post.id, start.reason);
|
||||
this.resources.activityLogRepo.insert({
|
||||
@@ -457,6 +477,7 @@ class AccountRuntimeLoop {
|
||||
}
|
||||
|
||||
postsRepo.markMessageSent(post.id);
|
||||
establishmentService.recordSendSuccess();
|
||||
messagesRepo.insert({
|
||||
postId: post.id, recipient: post.author, subject: post.author,
|
||||
message: processed.generatedMessage, sentAt: Date.now(),
|
||||
|
||||
Reference in new issue
Block a user