feat: show the conversation evidence behind each learning trajectory

Reviewers were asked to include or exclude learning trajectories while
only seeing metadata — credit class, confidence, status. The decisions
and their redacted context turns were already captured and stored, but
no endpoint returned them, so the evidence was unreachable from the UI.

Add GET /api/learning/trajectories/:id and an expandable row on the
learning page that fetches on demand.

Also widen SentMessageSchema to accept matrixEventId, imageOutcomeCredit
and imageOutcomeAt. The learning loop added these to SentMessageRecord
and they flow out of /api/messages, but the strict contract rejected
them, breaking the dashboard.

Enable generation with shadow mode off so eligible examples reach reply
prompts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jamie BruntonandClaude Opus 5 committed 2026-08-12 08:23:22 +01:00
1 parent 687abab82b
commit 142378df33
25 files changed
+836 -28

No files matched your search

+24
View File
@@ -32,4 +32,28 @@ export const LearningOverviewResponseSchema = z.strictObject({
export const LearningReviewRequestSchema = z.strictObject({ status: z.enum(['eligible', 'excluded']) });
export const LearningReviewResponseSchema = z.strictObject({ success: z.literal(true) });
export const LearningStageSchema = z.enum([
'rapport', 'answer', 'flirt', 'disclosure', 'media-invitation', 'handoff', 'unknown',
]);
export const LearningDecisionStatusSchema = z.enum(['eligible', 'excluded', 'invalidated', 'needs-review']);
export const LearningTurnSchema = z.strictObject({
eventId: z.string(), timestamp: z.number(), role: z.enum(['recipient', 'operator']),
actor: z.enum(['recipient', 'ai', 'human']),
kind: z.enum(['inbound-text', 'opener', 'automation-reply', 'manual-text', 'library-reciprocal']),
text: z.string(),
});
export const LearningDecisionSchema = z.strictObject({
id: z.string(), trajectoryId: z.string(), accountId: z.string(), decisionEventId: z.string(),
stage: LearningStageSchema, context: z.array(LearningTurnSchema), chosenResponse: z.string(),
turnsUntilOutcome: z.number().int(), isDirectMediaInvitation: z.boolean(),
qualityStatus: LearningDecisionStatusSchema, createdAt: z.number(), updatedAt: z.number(),
});
export const LearningTrajectoryDetailResponseSchema = z.strictObject({
trajectory: LearningTrajectorySchema,
decisions: z.array(LearningDecisionSchema),
});
export type LearningOverviewResponse = z.infer<typeof LearningOverviewResponseSchema>;
export type LearningTrajectoryDetailResponse = z.infer<typeof LearningTrajectoryDetailResponseSchema>;
export type LearningDecision = z.infer<typeof LearningDecisionSchema>;
export type LearningTurn = z.infer<typeof LearningTurnSchema>;
+4
View File
@@ -25,6 +25,10 @@ export const SentMessageSchema = z.strictObject({
postImageAnalysis: z.string().optional(),
postCreatedAt: z.number().optional(),
owner: RedditAccountOwnerSchema.optional(),
// Attribution/learning fields added by the conversation learning loop.
matrixEventId: z.string().nullable().optional(),
imageOutcomeCredit: z.string().nullable().optional(),
imageOutcomeAt: z.number().nullable().optional(),
});
export const MessagesResponseSchema = z.array(SentMessageSchema);
+6
View File
@@ -88,6 +88,11 @@ export const RedditAccountActivationResponseSchema = z.union([
z.strictObject({ ok: z.literal(true), account: RedditAccountSummarySchema }),
z.strictObject({ ok: z.literal(false), error: z.string() }),
]);
/** The removed account is echoed back so the caller can name what it deleted. */
export const RedditAccountRemovalResponseSchema = z.union([
z.strictObject({ ok: z.literal(true), account: RedditAccountSummarySchema }),
z.strictObject({ ok: z.literal(false), error: z.string() }),
]);
export type RedditSessionState = z.infer<typeof RedditSessionStateSchema>;
export type LoginStatusResponse = z.infer<typeof LoginStatusResponseSchema>;
@@ -96,3 +101,4 @@ export type RedditAccountSummary = z.infer<typeof RedditAccountSummarySchema>;
export type AccountEstablishmentState = z.infer<typeof AccountEstablishmentStateSchema>;
export type AccountEstablishmentSummary = z.infer<typeof AccountEstablishmentSummarySchema>;
export type RedditAccountOwner = z.infer<typeof RedditAccountOwnerSchema>;
export type RedditAccountRemovalResponse = z.infer<typeof RedditAccountRemovalResponseSchema>;