Add human-led conversation learning loop

This commit is contained in:
jjbrunton committed 2026-08-11 23:04:25 +01:00
1 parent bc42c55555
commit 687abab82b
52 files changed
+2112 -53

No files matched your search

+30 -7
View File
@@ -23,6 +23,7 @@ export type { InvitationCooldown } from './types.js';
import { fetchWithTimeout } from '../../utils/fetch-with-timeout.js';
import type { AccountAuthGate } from '../account-auth-context.js';
import { containsMediaRequest } from '../conversation-boundaries.js';
import type { ConversationLearningObserver } from '../conversation-learning-observer.js';
const ACTIVE_WINDOW_MS = 96 * 60 * 60 * 1000;
const ROOM_URL_PREFIX = 'https://chat.reddit.com/room/';
@@ -60,6 +61,11 @@ export type ConversationStartResult =
| { status: 'terminal'; reason: string }
| { status: 'retryable'; reason: string; retryAt: number };
export interface ChatSendReceipt {
eventId: string;
sentAt: number;
}
interface BuildChatOpts {
knownUsernames: Map<string, string>;
includeTimelineMessages?: boolean;
@@ -170,6 +176,7 @@ export class ChatService {
private accountAuth?: AccountAuthGate,
private outcomesRepo?: ConversationOutcomesRepository,
private conversationStateRepo?: ConversationStateRepository,
private conversationLearningObserver?: ConversationLearningObserver,
) {
this.cache = cache ?? new ChatCache(logger);
if (this.redditUsersRepo) {
@@ -608,38 +615,48 @@ export class ChatService {
* a raw Matrix room ID like `!abc:reddit.com`.
*/
async sendChatMessage(chatUrlOrRoomId: string, message: string, txnId?: string): Promise<boolean> {
if (!this.accountAuth?.isFullyAuthenticated()) return false;
return (await this.sendChatMessageDetailed(chatUrlOrRoomId, message, txnId)) !== null;
}
/** Send a reply and retain the authoritative Matrix event identity. */
async sendChatMessageDetailed(
chatUrlOrRoomId: string,
message: string,
txnId?: string,
): Promise<ChatSendReceipt | null> {
if (!this.accountAuth?.isFullyAuthenticated()) return null;
const active = this.getInvitationCooldown();
if (active && this.blocksExistingRoomSends(active)) {
const secondsLeft = Math.ceil((active.until - Date.now()) / 1000);
this.logger.warn(
`[chat] skipping sendChatMessage — send cooldown active for ${secondsLeft}s (${active.redditCode ?? 'rate-limited'})`,
);
return false;
return null;
}
return this.enqueue(async () => {
try {
const roomId = ChatService.extractRoomId(chatUrlOrRoomId);
if (!roomId) {
this.logger.error(`[chat] cannot extract room ID from "${chatUrlOrRoomId}"`);
return false;
return null;
}
const eventId = await this.withAuth(() => this.matrix.sendTextMessage(roomId, message, txnId));
this.logger.info(`[chat] sent message to ${roomId} (event_id=${eventId})`);
const chat = this.cache.get().find((c) => c.roomId === roomId);
const sentAt = Date.now();
if (chat) {
chat.lastMessageSnippet = message.slice(0, 200);
chat.lastMessageTime = Date.now();
chat.lastMessageTime = sentAt;
chat.isFromUs = true;
}
return true;
return { eventId, sentAt };
} catch (error) {
if (error instanceof MatrixRateLimitError) {
this.setInvitationCooldown(error, 'all-sends');
return false;
return null;
}
this.logger.error('[chat] sendChatMessage failed:', error);
return false;
return null;
}
});
}
@@ -1252,11 +1269,17 @@ export class ChatService {
this.logger.warn('[chat] failed to record conversation outcome:', err);
return false;
}
this.conversationLearningObserver?.observe(chat, performanceMessage, conversationState);
return wasMissingOutcome;
} else if (hasReply && !performanceMessage.gotResponse) {
this.messagesRepo.markGotResponse(performanceMessage.id);
this.logger.warn('[chat] response recorded without canonical outcome repository; excluded from learning');
}
this.conversationLearningObserver?.observe(
chat,
performanceMessage,
this.conversationStateRepo?.get(chat.roomId),
);
return false;
}