birdclaw 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +2 -4
  3. package/scripts/browser-perf.mjs +27 -0
  4. package/src/cli/command-context.ts +17 -0
  5. package/src/cli/register-analysis.ts +500 -0
  6. package/src/cli/register-compose.ts +40 -0
  7. package/src/cli/register-graph.ts +132 -0
  8. package/src/cli/register-inbox.ts +41 -0
  9. package/src/cli/register-storage.ts +106 -0
  10. package/src/cli.ts +30 -750
  11. package/src/components/AccountSwitcher.tsx +7 -15
  12. package/src/components/AvatarChip.tsx +1 -1
  13. package/src/components/AvatarPreload.ts +149 -0
  14. package/src/components/ConversationThread.tsx +4 -0
  15. package/src/components/EmbeddedTweetCard.tsx +4 -0
  16. package/src/components/FloatingPreview.tsx +382 -0
  17. package/src/components/MarkdownCitations.tsx +680 -0
  18. package/src/components/MarkdownViewer.tsx +7 -715
  19. package/src/components/ProfileAnalysisClient.ts +191 -0
  20. package/src/components/ProfileAnalysisStream.tsx +16 -185
  21. package/src/components/ProfilePreview.tsx +41 -81
  22. package/src/components/TimelineCard.tsx +18 -2
  23. package/src/components/TweetArticleCard.tsx +66 -0
  24. package/src/components/TweetRichText.tsx +33 -2
  25. package/src/components/links-controller.ts +162 -0
  26. package/src/components/links-model.ts +198 -0
  27. package/src/components/network-map-controller.ts +84 -0
  28. package/src/components/network-map-model.ts +255 -0
  29. package/src/components/useDebouncedValue.ts +12 -0
  30. package/src/components/useTimelineRouteData.ts +142 -170
  31. package/src/lib/analysis-runtime.ts +238 -0
  32. package/src/lib/api-client.ts +16 -100
  33. package/src/lib/api-contracts.ts +328 -0
  34. package/src/lib/archive-finder.ts +38 -0
  35. package/src/lib/archive-import-plan.ts +102 -0
  36. package/src/lib/archive-import.ts +170 -239
  37. package/src/lib/authored-live.ts +77 -182
  38. package/src/lib/backup.ts +335 -424
  39. package/src/lib/bird.ts +32 -1
  40. package/src/lib/blocks-write.ts +30 -26
  41. package/src/lib/blocks.ts +18 -20
  42. package/src/lib/database-metrics.ts +88 -0
  43. package/src/lib/database-migrations.ts +34 -0
  44. package/src/lib/database-schema.ts +312 -0
  45. package/src/lib/database-writer.ts +69 -0
  46. package/src/lib/db.ts +141 -334
  47. package/src/lib/dm-read-model.ts +533 -0
  48. package/src/lib/dms-live.ts +34 -97
  49. package/src/lib/follow-graph.ts +17 -27
  50. package/src/lib/import-repository.ts +138 -0
  51. package/src/lib/inbox.ts +2 -1
  52. package/src/lib/live-sync-engine.ts +209 -0
  53. package/src/lib/live-transport-gateway.ts +128 -0
  54. package/src/lib/mention-threads-live.ts +90 -176
  55. package/src/lib/mentions-export.ts +1 -1
  56. package/src/lib/mentions-live.ts +57 -225
  57. package/src/lib/moderation-target.ts +15 -4
  58. package/src/lib/moderation-write.ts +1 -1
  59. package/src/lib/mutes-write.ts +30 -26
  60. package/src/lib/openai-response-runtime.ts +251 -0
  61. package/src/lib/paginated-sync.ts +93 -0
  62. package/src/lib/period-digest.ts +116 -304
  63. package/src/lib/profile-analysis.ts +37 -111
  64. package/src/lib/queries.ts +6 -2380
  65. package/src/lib/query-actions.ts +437 -0
  66. package/src/lib/query-client.tsx +47 -0
  67. package/src/lib/query-read-model-shared.ts +52 -0
  68. package/src/lib/query-read-models.ts +5 -0
  69. package/src/lib/query-resource.ts +41 -0
  70. package/src/lib/query-status.ts +164 -0
  71. package/src/lib/research.ts +1 -1
  72. package/src/lib/runtime-services.ts +20 -0
  73. package/src/lib/search-discussion.ts +75 -279
  74. package/src/lib/server-runtime-services.ts +30 -0
  75. package/src/lib/sqlite.ts +53 -14
  76. package/src/lib/streaming-ingestion.ts +240 -0
  77. package/src/lib/sync-cache.ts +6 -1
  78. package/src/lib/sync-plan.ts +175 -0
  79. package/src/lib/timeline-collections-live.ts +83 -256
  80. package/src/lib/timeline-live.ts +86 -235
  81. package/src/lib/timeline-read-model.ts +1191 -0
  82. package/src/lib/tweet-render.ts +78 -0
  83. package/src/lib/tweet-repository.ts +156 -0
  84. package/src/lib/tweet-search-live.ts +63 -166
  85. package/src/lib/types.ts +8 -0
  86. package/src/lib/ui.ts +1 -1
  87. package/src/lib/web-sync.ts +67 -50
  88. package/src/lib/whois.ts +2 -1
  89. package/src/router.tsx +1 -1
  90. package/src/routes/__root.tsx +11 -21
  91. package/src/routes/api/action.tsx +1 -1
  92. package/src/routes/api/conversation.tsx +1 -1
  93. package/src/routes/api/query.tsx +32 -26
  94. package/src/routes/api/status.tsx +6 -2
  95. package/src/routes/api/sync.tsx +5 -2
  96. package/src/routes/blocks.tsx +97 -131
  97. package/src/routes/data-sources.tsx +17 -25
  98. package/src/routes/dms.tsx +168 -167
  99. package/src/routes/inbox.tsx +63 -57
  100. package/src/routes/links.tsx +31 -329
  101. package/src/routes/network-map.tsx +41 -344
  102. package/src/routes/rate-limits.tsx +17 -21
  103. package/vite.config.ts +0 -2
@@ -0,0 +1,128 @@
1
+ import { Effect } from "effect";
2
+ import {
3
+ getAuthenticatedBirdAccountEffect,
4
+ listBookmarkedTweetsViaBirdEffect,
5
+ listDirectMessagesViaBirdEffect,
6
+ listFollowUsersViaBirdEffect,
7
+ listHomeTimelineViaBirdEffect,
8
+ listLikedTweetsViaBirdEffect,
9
+ listMentionsViaBirdEffect,
10
+ listThreadViaBirdEffect,
11
+ searchTweetsViaBirdEffect,
12
+ } from "./bird";
13
+ import {
14
+ getTransportStatus,
15
+ getTweetByIdEffect,
16
+ listBookmarkedTweetsViaXurl,
17
+ listDirectMessageEventsViaXurlEffect,
18
+ listFollowUsersViaXurl,
19
+ listHomeTimelineViaXurlEffect,
20
+ listLikedTweetsViaXurl,
21
+ listMentionsViaXurl,
22
+ listUserTweets,
23
+ lookupAuthenticatedOAuth2UserEffect,
24
+ lookupAuthenticatedUser,
25
+ lookupUsersByHandles,
26
+ searchRecentByConversationIdEffect,
27
+ searchRecentTweetsEffect,
28
+ } from "./xurl";
29
+
30
+ export interface BirdReadTransport {
31
+ getAuthenticatedAccount: typeof getAuthenticatedBirdAccountEffect;
32
+ listBookmarks: typeof listBookmarkedTweetsViaBirdEffect;
33
+ listDirectMessages: typeof listDirectMessagesViaBirdEffect;
34
+ listFollowUsers: typeof listFollowUsersViaBirdEffect;
35
+ listHomeTimeline: typeof listHomeTimelineViaBirdEffect;
36
+ listLikes: typeof listLikedTweetsViaBirdEffect;
37
+ listMentions: typeof listMentionsViaBirdEffect;
38
+ listThread: typeof listThreadViaBirdEffect;
39
+ searchTweets: typeof searchTweetsViaBirdEffect;
40
+ }
41
+
42
+ export interface XurlReadTransport {
43
+ getTransportStatus(
44
+ ...args: Parameters<typeof getTransportStatus>
45
+ ): Effect.Effect<Awaited<ReturnType<typeof getTransportStatus>>, Error>;
46
+ getTweetById: typeof getTweetByIdEffect;
47
+ listBookmarks(
48
+ ...args: Parameters<typeof listBookmarkedTweetsViaXurl>
49
+ ): Effect.Effect<
50
+ Awaited<ReturnType<typeof listBookmarkedTweetsViaXurl>>,
51
+ Error
52
+ >;
53
+ listDirectMessages: typeof listDirectMessageEventsViaXurlEffect;
54
+ listFollowUsers(
55
+ ...args: Parameters<typeof listFollowUsersViaXurl>
56
+ ): Effect.Effect<Awaited<ReturnType<typeof listFollowUsersViaXurl>>, Error>;
57
+ listHomeTimeline: typeof listHomeTimelineViaXurlEffect;
58
+ listLikes(
59
+ ...args: Parameters<typeof listLikedTweetsViaXurl>
60
+ ): Effect.Effect<Awaited<ReturnType<typeof listLikedTweetsViaXurl>>, Error>;
61
+ listMentions(
62
+ ...args: Parameters<typeof listMentionsViaXurl>
63
+ ): Effect.Effect<Awaited<ReturnType<typeof listMentionsViaXurl>>, Error>;
64
+ listUserTweets(
65
+ ...args: Parameters<typeof listUserTweets>
66
+ ): Effect.Effect<Awaited<ReturnType<typeof listUserTweets>>, Error>;
67
+ lookupAuthenticatedOAuth2User: typeof lookupAuthenticatedOAuth2UserEffect;
68
+ lookupAuthenticatedUser(
69
+ ...args: Parameters<typeof lookupAuthenticatedUser>
70
+ ): Effect.Effect<Awaited<ReturnType<typeof lookupAuthenticatedUser>>, Error>;
71
+ lookupUsersByHandles(
72
+ ...args: Parameters<typeof lookupUsersByHandles>
73
+ ): Effect.Effect<Awaited<ReturnType<typeof lookupUsersByHandles>>, Error>;
74
+ searchConversation: typeof searchRecentByConversationIdEffect;
75
+ searchRecentTweets: typeof searchRecentTweetsEffect;
76
+ }
77
+
78
+ export interface LiveTransportGateway {
79
+ bird: BirdReadTransport;
80
+ xurl: XurlReadTransport;
81
+ }
82
+
83
+ function fromPromise<T>(run: () => PromiseLike<T>): Effect.Effect<T, Error> {
84
+ return Effect.tryPromise({
85
+ try: run,
86
+ catch: (error) =>
87
+ error instanceof Error ? error : new Error(String(error)),
88
+ });
89
+ }
90
+
91
+ export const liveTransportGateway: LiveTransportGateway = {
92
+ bird: {
93
+ getAuthenticatedAccount: () => getAuthenticatedBirdAccountEffect(),
94
+ listBookmarks: (options) => listBookmarkedTweetsViaBirdEffect(options),
95
+ listDirectMessages: (options) => listDirectMessagesViaBirdEffect(options),
96
+ listFollowUsers: (options) => listFollowUsersViaBirdEffect(options),
97
+ listHomeTimeline: (options) => listHomeTimelineViaBirdEffect(options),
98
+ listLikes: (options) => listLikedTweetsViaBirdEffect(options),
99
+ listMentions: (options) => listMentionsViaBirdEffect(options),
100
+ listThread: (options) => listThreadViaBirdEffect(options),
101
+ searchTweets: (query, options) => searchTweetsViaBirdEffect(query, options),
102
+ },
103
+ xurl: {
104
+ getTransportStatus: (...args) =>
105
+ fromPromise(() => getTransportStatus(...args)),
106
+ getTweetById: (id, options) => getTweetByIdEffect(id, options),
107
+ listBookmarks: (...args) =>
108
+ fromPromise(() => listBookmarkedTweetsViaXurl(...args)),
109
+ listDirectMessages: (options) =>
110
+ listDirectMessageEventsViaXurlEffect(options),
111
+ listFollowUsers: (...args) =>
112
+ fromPromise(() => listFollowUsersViaXurl(...args)),
113
+ listHomeTimeline: (options) => listHomeTimelineViaXurlEffect(options),
114
+ listLikes: (...args) => fromPromise(() => listLikedTweetsViaXurl(...args)),
115
+ listMentions: (...args) => fromPromise(() => listMentionsViaXurl(...args)),
116
+ listUserTweets: (...args) => fromPromise(() => listUserTweets(...args)),
117
+ lookupAuthenticatedOAuth2User: (username) =>
118
+ lookupAuthenticatedOAuth2UserEffect(username),
119
+ lookupAuthenticatedUser: (...args) =>
120
+ fromPromise(() => lookupAuthenticatedUser(...args)),
121
+ lookupUsersByHandles: (...args) =>
122
+ fromPromise(() => lookupUsersByHandles(...args)),
123
+ searchConversation: (conversationId, options) =>
124
+ searchRecentByConversationIdEffect(conversationId, options),
125
+ searchRecentTweets: (query, options) =>
126
+ searchRecentTweetsEffect(query, options),
127
+ },
128
+ };
@@ -1,9 +1,11 @@
1
1
  import type { Database } from "./sqlite";
2
2
  import { Effect } from "effect";
3
- import { listThreadViaBirdEffect } from "./bird";
3
+ import { databaseWriteEffect } from "./database-writer";
4
4
  import { getNativeDb } from "./db";
5
5
  import { runEffectPromise } from "./effect-runtime";
6
- import { buildMediaJsonFromIncludes, countTweetMedia } from "./media-includes";
6
+ import { liveTransportGateway } from "./live-transport-gateway";
7
+ import { resolveLiveSyncAccount } from "./live-sync-engine";
8
+ import { runSyncPlanEffect } from "./sync-plan";
7
9
  import type {
8
10
  XurlMentionData,
9
11
  XurlMentionsResponse,
@@ -11,9 +13,7 @@ import type {
11
13
  XurlMediaItem,
12
14
  XurlTweetsResponse,
13
15
  } from "./types";
14
- import { upsertTweetAccountEdge } from "./tweet-account-edges";
15
- import { ensureStubProfileForXUser, upsertProfileFromXUser } from "./x-profile";
16
- import { getTweetByIdEffect, searchRecentByConversationIdEffect } from "./xurl";
16
+ import { ingestTweetPayload } from "./tweet-repository";
17
17
 
18
18
  const DEFAULT_LIMIT = 30;
19
19
  const DEFAULT_DELAY_MS = 1500;
@@ -106,40 +106,6 @@ function getRemainingThreadTimeoutMs(
106
106
  }
107
107
  return remainingMs;
108
108
  }
109
- function replaceTweetFts(db: Database, tweetId: string, text: string) {
110
- db.prepare("delete from tweets_fts where tweet_id = ?").run(tweetId);
111
- db.prepare("insert into tweets_fts (tweet_id, text) values (?, ?)").run(
112
- tweetId,
113
- text,
114
- );
115
- }
116
-
117
- function resolveAccount(db: Database, accountId?: string) {
118
- const row = accountId
119
- ? (db
120
- .prepare("select id, handle from accounts where id = ?")
121
- .get(accountId) as { id: string; handle: string } | undefined)
122
- : (db
123
- .prepare(
124
- `
125
- select id, handle
126
- from accounts
127
- order by is_default desc, created_at asc
128
- limit 1
129
- `,
130
- )
131
- .get() as { id: string; handle: string } | undefined);
132
-
133
- if (!row) {
134
- throw new Error(`Unknown account: ${accountId ?? "default"}`);
135
- }
136
-
137
- return {
138
- accountId: row.id,
139
- handle: row.handle.replace(/^@/, "").toLowerCase(),
140
- };
141
- }
142
-
143
109
  function getReplyToId(tweet: XurlMentionData) {
144
110
  return tweet.referenced_tweets?.find((entry) => entry.type === "replied_to")
145
111
  ?.id;
@@ -353,84 +319,32 @@ function mergeMentionThreadIntoLocalStore({
353
319
  const usersById = new Map(
354
320
  (payload.includes?.users ?? []).map((user) => [user.id, user]),
355
321
  );
356
- const upsertTweet = db.prepare(
357
- `
358
- insert into tweets (
359
- id, account_id, author_profile_id, kind, text, created_at,
360
- is_replied, reply_to_id, like_count, media_count, bookmarked, liked,
361
- entities_json, media_json, quoted_tweet_id
362
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, null)
363
- on conflict(id) do update set
364
- account_id = excluded.account_id,
365
- author_profile_id = excluded.author_profile_id,
366
- kind = case
367
- when tweets.kind in ('authored', 'home', 'mention') then tweets.kind
368
- when excluded.kind in ('authored', 'home', 'mention') then excluded.kind
369
- else coalesce(nullif(tweets.kind, ''), excluded.kind)
370
- end,
371
- text = excluded.text,
372
- created_at = excluded.created_at,
373
- is_replied = max(tweets.is_replied, excluded.is_replied),
374
- reply_to_id = coalesce(excluded.reply_to_id, tweets.reply_to_id),
375
- like_count = excluded.like_count,
376
- media_count = max(tweets.media_count, excluded.media_count),
377
- entities_json = excluded.entities_json,
378
- media_json = case
379
- when excluded.media_json not in ('', '[]', 'null') then excluded.media_json
380
- else tweets.media_json
381
- end,
382
- bookmarked = tweets.bookmarked,
383
- liked = tweets.liked
384
- `,
385
- );
386
-
387
- db.transaction(() => {
388
- const seenAt = new Date().toISOString();
389
- for (const tweet of payload.data) {
390
- const author =
391
- usersById.get(tweet.author_id) ??
392
- ({
393
- id: tweet.author_id,
394
- username: `user_${tweet.author_id}`,
395
- name: `user_${tweet.author_id}`,
396
- } as const);
397
- const profile = usersById.has(tweet.author_id)
398
- ? upsertProfileFromXUser(db, author)
399
- : ensureStubProfileForXUser(db, tweet.author_id);
400
- const handle = author.username.toLowerCase();
401
- const kind = mentionIds.has(tweet.id)
402
- ? "mention"
403
- : handle === accountHandle
404
- ? "home"
405
- : "thread";
406
- const replyToId = getReplyToId(tweet);
407
- upsertTweet.run(
408
- tweet.id,
409
- accountId,
410
- profile.profile.id,
411
- kind,
412
- tweet.text,
413
- tweet.created_at,
414
- replyToId ? 1 : 0,
415
- replyToId ?? null,
416
- Number(tweet.public_metrics?.like_count ?? 0),
417
- countTweetMedia(tweet),
418
- JSON.stringify(tweet.entities ?? {}),
419
- buildMediaJsonFromIncludes(tweet, payload.includes?.media),
420
- );
421
- if (writeThreadContextEdges) {
422
- upsertTweetAccountEdge(db, {
423
- accountId,
424
- tweetId: tweet.id,
425
- kind: "thread_context",
426
- source,
427
- seenAt,
428
- rawJson: JSON.stringify(tweet),
429
- });
430
- }
431
- replaceTweetFts(db, tweet.id, tweet.text);
432
- }
433
- })();
322
+ const groups = new Map<"mention" | "home" | "thread", typeof payload.data>([
323
+ ["mention", []],
324
+ ["home", []],
325
+ ["thread", []],
326
+ ]);
327
+ for (const tweet of payload.data) {
328
+ const handle = usersById.get(tweet.author_id)?.username.toLowerCase();
329
+ const kind = mentionIds.has(tweet.id)
330
+ ? "mention"
331
+ : handle === accountHandle
332
+ ? "home"
333
+ : "thread";
334
+ groups.get(kind)?.push(tweet);
335
+ }
336
+ for (const [kind, data] of groups) {
337
+ if (data.length === 0) continue;
338
+ ingestTweetPayload(db, {
339
+ accountId,
340
+ payload: { ...payload, data },
341
+ kind,
342
+ edgeKind: writeThreadContextEdges ? "thread_context" : undefined,
343
+ markRepliesAsReplied: true,
344
+ replaceSecondaryKind: true,
345
+ source,
346
+ });
347
+ }
434
348
  }
435
349
 
436
350
  function fetchConversationViaRecentSearchEffect({
@@ -447,39 +361,29 @@ function fetchConversationViaRecentSearchEffect({
447
361
  deadlineMs: number;
448
362
  }) {
449
363
  return Effect.gen(function* () {
450
- const pages: XurlTweetsResponse[] = [];
451
- let nextToken: string | undefined;
452
- let pageCount = 0;
453
-
454
- do {
455
- const payload = yield* searchRecentByConversationIdEffect(
456
- conversationId,
457
- {
458
- maxResults: MAX_XURL_SEARCH_RESULTS,
459
- paginationToken: nextToken,
460
- timeoutMs: yield* trySync(() =>
461
- getRemainingThreadTimeoutMs(deadlineMs, timeoutMs),
364
+ const result = yield* runSyncPlanEffect({
365
+ fetchPage: ({ cursor }) =>
366
+ trySync(() => getRemainingThreadTimeoutMs(deadlineMs, timeoutMs)).pipe(
367
+ Effect.flatMap((remainingTimeoutMs) =>
368
+ liveTransportGateway.xurl.searchConversation(conversationId, {
369
+ maxResults: MAX_XURL_SEARCH_RESULTS,
370
+ paginationToken: cursor,
371
+ timeoutMs: remainingTimeoutMs,
372
+ }),
462
373
  ),
463
- },
464
- );
465
- pages.push(payload);
466
- nextToken =
467
- typeof payload.meta?.next_token === "string"
468
- ? payload.meta.next_token
469
- : undefined;
470
- pageCount += 1;
471
- } while (
472
- (all || maxPages !== undefined) &&
473
- nextToken &&
474
- (maxPages === undefined || pageCount < maxPages)
475
- );
476
-
477
- const payload = mergePayloads(pages);
374
+ ),
375
+ getNextCursor: (page) =>
376
+ typeof page.meta?.next_token === "string"
377
+ ? page.meta.next_token
378
+ : undefined,
379
+ maxPages: all || maxPages !== undefined ? maxPages : 1,
380
+ });
381
+ const payload = mergePayloads(result.pages);
478
382
  const paginationRequested = all || maxPages !== undefined;
479
383
  return {
480
384
  payload,
481
- pages: pageCount,
482
- truncated: paginationRequested && Boolean(nextToken),
385
+ pages: result.pages.length,
386
+ truncated: paginationRequested && Boolean(result.nextCursor),
483
387
  generalReadTweets: payload.data.length,
484
388
  };
485
389
  });
@@ -511,11 +415,14 @@ function fetchParentChainViaXurlEffect({
511
415
  let shouldUseRawAnchor = Boolean(rawAnchorPayload);
512
416
 
513
417
  if (!nextParentId) {
514
- const anchorPayload = yield* getTweetByIdEffect(mention.id, {
515
- timeoutMs: yield* trySync(() =>
516
- getRemainingThreadTimeoutMs(deadlineMs, timeoutMs),
517
- ),
518
- });
418
+ const anchorPayload = yield* liveTransportGateway.xurl.getTweetById(
419
+ mention.id,
420
+ {
421
+ timeoutMs: yield* trySync(() =>
422
+ getRemainingThreadTimeoutMs(deadlineMs, timeoutMs),
423
+ ),
424
+ },
425
+ );
519
426
  pages.push(anchorPayload);
520
427
  generalReadTweets += anchorPayload.data.length;
521
428
  const anchorTweet = anchorPayload.data[0];
@@ -547,11 +454,14 @@ function fetchParentChainViaXurlEffect({
547
454
  }
548
455
 
549
456
  fallbackDepth += 1;
550
- const parentPayload = yield* getTweetByIdEffect(nextParentId, {
551
- timeoutMs: yield* trySync(() =>
552
- getRemainingThreadTimeoutMs(deadlineMs, timeoutMs),
553
- ),
554
- });
457
+ const parentPayload = yield* liveTransportGateway.xurl.getTweetById(
458
+ nextParentId,
459
+ {
460
+ timeoutMs: yield* trySync(() =>
461
+ getRemainingThreadTimeoutMs(deadlineMs, timeoutMs),
462
+ ),
463
+ },
464
+ );
555
465
  pages.push(parentPayload);
556
466
  generalReadTweets += parentPayload.data.length;
557
467
  const parentTweet = parentPayload.data[0];
@@ -742,7 +652,9 @@ export function syncMentionThreadsEffect({
742
652
  parseNonNegativeInteger(maxPages, "--max-pages"),
743
653
  );
744
654
  const db = yield* trySync(() => getNativeDb());
745
- const resolvedAccount = yield* trySync(() => resolveAccount(db, account));
655
+ const resolvedAccount = yield* trySync(() =>
656
+ resolveLiveSyncAccount(db, account),
657
+ );
746
658
  const mentions = yield* trySync(() =>
747
659
  tweetIds
748
660
  ? listMentionsByIds(
@@ -778,22 +690,24 @@ export function syncMentionThreadsEffect({
778
690
  }
779
691
  const fetchEffect: Effect.Effect<ThreadFetchResult, unknown, never> =
780
692
  parsedMode === "bird"
781
- ? listThreadViaBirdEffect({
782
- tweetId: mention.id,
783
- all,
784
- maxPages: parsedMaxPages,
785
- timeoutMs: parsedTimeoutMs,
786
- }).pipe(
787
- Effect.map((payload) => ({
788
- strategy: "bird" as const,
789
- payload,
790
- pages: undefined,
791
- fallbackDepth: undefined,
792
- generalReadTweets: 0,
793
- truncated: undefined,
794
- warnings: [] as string[],
795
- })),
796
- )
693
+ ? liveTransportGateway.bird
694
+ .listThread({
695
+ tweetId: mention.id,
696
+ all,
697
+ maxPages: parsedMaxPages,
698
+ timeoutMs: parsedTimeoutMs,
699
+ })
700
+ .pipe(
701
+ Effect.map((payload) => ({
702
+ strategy: "bird" as const,
703
+ payload,
704
+ pages: undefined,
705
+ fallbackDepth: undefined,
706
+ generalReadTweets: 0,
707
+ truncated: undefined,
708
+ warnings: [] as string[],
709
+ })),
710
+ )
797
711
  : fetchThreadContextViaXurlEffect({
798
712
  mention,
799
713
  all,
@@ -803,11 +717,11 @@ export function syncMentionThreadsEffect({
803
717
  });
804
718
  const fetched = yield* fetchEffect.pipe(
805
719
  Effect.flatMap((fetchResult) =>
806
- trySync(() =>
720
+ databaseWriteEffect((writeDb) =>
807
721
  mergeMentionThreadIntoLocalStore({
808
- db,
722
+ db: writeDb,
809
723
  accountId: resolvedAccount.accountId,
810
- accountHandle: resolvedAccount.handle,
724
+ accountHandle: resolvedAccount.username.toLowerCase(),
811
725
  mentionIds: mentionIdSet,
812
726
  payload: fetchResult.payload,
813
727
  source: parsedMode,
@@ -1,4 +1,4 @@
1
- import { listTimelineItems } from "./queries";
1
+ import { listTimelineItems } from "./timeline-read-model";
2
2
  import { renderTweetMarkdown, renderTweetPlainText } from "./tweet-render";
3
3
  import type {
4
4
  ReplyFilter,