Skip to content

Commit 9a6b0c1

Browse files
committed
feat: populate minimal parent conversation hint
1 parent 44b0612 commit 9a6b0c1

6 files changed

Lines changed: 77 additions & 4 deletions

File tree

src/engagement/conversationBundle.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ export interface ConversationBundleInput {
3838
sourceMetadata?: Record<string, unknown>;
3939
}
4040

41+
export interface ConversationParentHint {
42+
tweetId?: string;
43+
conversationId?: string;
44+
authorId?: string;
45+
}
46+
4147
function readString(metadata: Record<string, unknown> | undefined, key: string): string | undefined {
4248
const value = metadata?.[key];
4349
return typeof value === "string" ? value : undefined;
@@ -74,6 +80,24 @@ function buildAuthorContext(input: ConversationBundleInput): ConversationAuthorC
7480
};
7581
}
7682

83+
export function buildConversationParentRef(
84+
hint: ConversationParentHint | undefined
85+
): ConversationParentRef | undefined {
86+
if (!hint) {
87+
return undefined;
88+
}
89+
90+
if (!hint.tweetId && !hint.conversationId && !hint.authorId) {
91+
return undefined;
92+
}
93+
94+
return {
95+
tweetId: hint.tweetId,
96+
conversationId: hint.conversationId,
97+
authorId: hint.authorId,
98+
};
99+
}
100+
77101
export function maybeBuildConversationBundle(input: ConversationBundleInput): ConversationBundle | undefined {
78102
const bundle: ConversationBundle = {
79103
sourceTweet: buildSourceTweet(input),

src/worker/pollMentions.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ import {
7070
buildRawTriggerInputFromMention,
7171
toCanonicalExecutionInput,
7272
} from "../engagement/candidateBoundary.js";
73-
import { maybeBuildConversationBundle } from "../engagement/conversationBundle.js";
73+
import {
74+
buildConversationParentRef,
75+
maybeBuildConversationBundle,
76+
} from "../engagement/conversationBundle.js";
7477
import {
7578
runWritePreflight,
7679
releaseWritePreflight,
@@ -337,6 +340,12 @@ export async function processCanonicalMention(
337340
const engagementCandidate = buildEngagementCandidate(rawTriggerInput);
338341
const conversationBundle = maybeBuildConversationBundle({
339342
candidate: engagementCandidate,
343+
parentRef: buildConversationParentRef({
344+
tweetId:
345+
mention.referenced_tweets?.find((ref) => ref.type === "replied_to" || ref.type === "quoted")
346+
?.id,
347+
conversationId: mention.conversation_id ?? undefined,
348+
}),
340349
sourceTweet: {
341350
tweetId: engagementCandidate.tweetId,
342351
conversationId: engagementCandidate.conversationId,

src/worker/pollTimelineEngagement.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import {
1515
buildRawTriggerInputFromTimelineCandidate,
1616
toCanonicalExecutionInput,
1717
} from "../engagement/candidateBoundary.js";
18-
import { maybeBuildConversationBundle } from "../engagement/conversationBundle.js";
18+
import {
19+
buildConversationParentRef,
20+
maybeBuildConversationBundle,
21+
} from "../engagement/conversationBundle.js";
1922
import { logInfo, logWarn } from "../ops/logger.js";
2023
import { isPostingDisabled } from "../ops/launchGate.js";
2124
import { withCircuitBreaker } from "../ops/llmCircuitBreaker.js";
@@ -368,6 +371,10 @@ export async function runTimelineEngagementIteration(): Promise<TimelineIteratio
368371
const engagementCandidate = buildEngagementCandidate(rawTriggerInput);
369372
const conversationBundle = maybeBuildConversationBundle({
370373
candidate: engagementCandidate,
374+
parentRef: buildConversationParentRef({
375+
tweetId: candidate.referencedTweetIds[0],
376+
conversationId: candidate.conversationId,
377+
}),
371378
sourceTweet: {
372379
tweetId: engagementCandidate.tweetId,
373380
conversationId: engagementCandidate.conversationId,

tests/engagement/candidateBoundary.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import {
55
buildRawTriggerInputFromTimelineCandidate,
66
toCanonicalExecutionInput,
77
} from "../../src/engagement/candidateBoundary.js";
8-
import { maybeBuildConversationBundle } from "../../src/engagement/conversationBundle.js";
8+
import {
9+
buildConversationParentRef,
10+
maybeBuildConversationBundle,
11+
} from "../../src/engagement/conversationBundle.js";
912

1013
describe("candidateBoundary", () => {
1114
it("normalizes mention ingress into the shared candidate boundary", () => {
@@ -23,6 +26,10 @@ describe("candidateBoundary", () => {
2326
const candidate = buildEngagementCandidate(raw);
2427
const bundle = maybeBuildConversationBundle({
2528
candidate,
29+
parentRef: buildConversationParentRef({
30+
tweetId: "parent-mention-1",
31+
conversationId: candidate.conversationId,
32+
}),
2633
sourceTweet: {
2734
tweetId: candidate.tweetId,
2835
conversationId: candidate.conversationId,
@@ -44,6 +51,7 @@ describe("candidateBoundary", () => {
4451
expect(candidate.candidateId).toBe("mention-1");
4552
expect(candidate.normalizedText).toBe("Hello @Gnomes_onchain, can you help?");
4653
expect(bundle?.sourceTweet?.tweetId).toBe("mention-1");
54+
expect(bundle?.parentRef?.tweetId).toBe("parent-mention-1");
4755
expect(bundle?.authorContext?.authorHandle).toBe("Alice");
4856
expect(canonical.event_id).toBe("mention-1");
4957
expect(canonical.trigger_type).toBe("mention");
@@ -92,6 +100,10 @@ describe("candidateBoundary", () => {
92100
const candidate = buildEngagementCandidate(raw);
93101
const bundle = maybeBuildConversationBundle({
94102
candidate,
103+
parentRef: buildConversationParentRef({
104+
tweetId: "parent-tweet-1",
105+
conversationId: candidate.conversationId,
106+
}),
95107
sourceTweet: {
96108
tweetId: candidate.tweetId,
97109
conversationId: candidate.conversationId,
@@ -113,6 +125,7 @@ describe("candidateBoundary", () => {
113125
expect(candidate.candidateId).toBe("timeline:tweet-1");
114126
expect(candidate.normalizedText).toBe("A thoughtful timeline reply with a clear question?");
115127
expect(bundle?.sourceTweet?.tweetId).toBe("tweet-1");
128+
expect(bundle?.parentRef?.tweetId).toBe("parent-tweet-1");
116129
expect(bundle?.authorContext?.sourceAccount).toBe("timeline");
117130
expect(canonical.event_id).toBe("timeline:tweet-1");
118131
expect(canonical.trigger_type).toBe("reply");
@@ -133,9 +146,16 @@ describe("candidateBoundary", () => {
133146
};
134147
const candidate = buildEngagementCandidate(raw);
135148

136-
const bundle = maybeBuildConversationBundle({ candidate });
149+
const bundle = maybeBuildConversationBundle({
150+
candidate,
151+
parentRef: buildConversationParentRef({
152+
tweetId: "parent-2",
153+
conversationId: candidate.conversationId,
154+
}),
155+
});
137156

138157
expect(bundle?.sourceTweet?.tweetId).toBe("mention-2");
158+
expect(bundle?.parentRef?.tweetId).toBe("parent-2");
139159
expect(bundle?.authorContext?.authorId).toBe("author-3");
140160
expect(bundle?.sourceMetadata?.authorHandle).toBe("alice");
141161
});

tests/integration/pipeline/mentionPipelineConsentFlow.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const maybeBuildConversationBundleMock = vi.fn(
6363
normalizedText: input.candidate.normalizedText,
6464
discoveredAt: input.candidate.discoveredAt,
6565
},
66+
parentRef: input.parentRef,
6667
authorContext: input.authorContext,
6768
sourceMetadata: input.sourceMetadata ?? input.candidate.sourceMetadata,
6869
})
@@ -111,6 +112,8 @@ vi.mock("../../../src/engagement/candidateBoundary.js", () => ({
111112
}));
112113

113114
vi.mock("../../../src/engagement/conversationBundle.js", () => ({
115+
buildConversationParentRef: (hint: { tweetId?: string; conversationId?: string; authorId?: string }) =>
116+
hint?.tweetId || hint?.conversationId || hint?.authorId ? hint : undefined,
114117
maybeBuildConversationBundle: maybeBuildConversationBundleMock,
115118
}));
116119

@@ -187,6 +190,10 @@ describe("mention pipeline consent flow", () => {
187190
expect(buildRawTriggerInputMock).toHaveBeenCalledTimes(1);
188191
expect(buildEngagementCandidateMock).toHaveBeenCalledTimes(1);
189192
expect(maybeBuildConversationBundleMock).toHaveBeenCalledTimes(1);
193+
expect(maybeBuildConversationBundleMock.mock.calls[0]?.[0].parentRef).toMatchObject({
194+
tweetId: "bot-1",
195+
conversationId: "conv-1",
196+
});
190197
expect(toCanonicalExecutionInputMock).toHaveBeenCalledTimes(1);
191198
expect(toCanonicalExecutionInputMock.mock.calls[0]?.[1]).toBeDefined();
192199
});

tests/worker/pollTimelineEngagement.integration.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ const maybeBuildConversationBundleMock = vi.fn(
9090
normalizedText: input.candidate.normalizedText,
9191
discoveredAt: input.candidate.discoveredAt,
9292
},
93+
parentRef: input.parentRef,
9394
authorContext: input.authorContext,
9495
sourceMetadata: input.sourceMetadata ?? input.candidate.sourceMetadata,
9596
})
@@ -171,6 +172,8 @@ vi.mock("../../src/engagement/candidateBoundary.js", () => ({
171172
}));
172173

173174
vi.mock("../../src/engagement/conversationBundle.js", () => ({
175+
buildConversationParentRef: (hint: { tweetId?: string; conversationId?: string; authorId?: string }) =>
176+
hint?.tweetId || hint?.conversationId || hint?.authorId ? hint : undefined,
174177
maybeBuildConversationBundle: maybeBuildConversationBundleMock,
175178
}));
176179

@@ -334,6 +337,9 @@ describe("timeline engagement worker", () => {
334337
expect(handleEventMock).toHaveBeenCalledTimes(1);
335338
expect(replyMock).toHaveBeenCalledTimes(1);
336339
expect(maybeBuildConversationBundleMock).toHaveBeenCalledTimes(1);
340+
expect(maybeBuildConversationBundleMock.mock.calls[0]?.[0].parentRef).toMatchObject({
341+
conversationId: "conv-1",
342+
});
337343
expect(buildRawTriggerInputMock).toHaveBeenCalledTimes(1);
338344
expect(buildEngagementCandidateMock).toHaveBeenCalledTimes(1);
339345
expect(toCanonicalExecutionInputMock).toHaveBeenCalledTimes(1);

0 commit comments

Comments
 (0)