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:
Jamie BruntonandClaude Opus 5 committed 2026-08-11 09:14:47 +01:00
1 parent c43e534cf1
commit 89eb4583f8
25 files changed
+1425 -11

No files matched your search

+32
View File
@@ -240,6 +240,7 @@ describe('account runtime isolation', () => {
conversationAutomation: { hasActionableReplies },
chatService: { getInvitationCooldown: () => null },
rateLimiter: { getTimers: () => ({ nextMessageAvailableAt: null }) },
establishmentService: { isSendable: () => true },
} as unknown as RedditAccountRuntimeResources,
{ start: vi.fn(), stop: vi.fn() },
);
@@ -250,6 +251,37 @@ describe('account runtime isolation', () => {
expect(await runtime.canAcceptPost()).toBe(true);
});
it('does not accept new outreach while the account is not established yet', async () => {
const stored = account('too_new');
const isSendable = vi.fn().mockReturnValue(false);
const runtime = new RedditAccountRuntime(
{
accountId: stored.accountId,
redditUsername: stored.redditUsername,
matrixUserId: stored.matrixUserId!,
cookies: [],
matrixCredentials: JSON.parse(stored.matrixCredentialsJson),
updatedAt: stored.updatedAt,
},
{
accountAuth: {
isFullyAuthenticated: () => true,
ensureFullyAuthenticated: async () => true,
},
conversationAutomation: { hasActionableReplies: vi.fn().mockResolvedValue(false) },
chatService: { getInvitationCooldown: () => null },
rateLimiter: { getTimers: () => ({ nextMessageAvailableAt: null }) },
establishmentService: { isSendable },
} as unknown as RedditAccountRuntimeResources,
{ start: vi.fn(), stop: vi.fn() },
);
await runtime.start();
expect(await runtime.canAcceptPost()).toBe(false);
isSendable.mockReturnValue(true);
expect(await runtime.canAcceptPost()).toBe(true);
});
it('uses the Reddit fullname for auth while retaining the Matrix runtime owner', () => {
expect(runtimeAuthenticatedAccount(
{ matrixUserId: '@t2_1u57iqxxbv:reddit.com' },