From 7df187c95c5c392cc14abef6b705da7665cd7cab Mon Sep 17 00:00:00 2001 From: Robin Date: Sat, 4 Jul 2026 14:55:23 +0200 Subject: [PATCH 1/4] fix targeted archived thread sync --- CHANGELOG.md | 6 +++ internal/cli/cli_test.go | 4 ++ internal/discord/client.go | 6 +++ internal/syncer/channel_catalog.go | 32 +++++++++++ internal/syncer/channel_catalog_test.go | 70 +++++++++++++++++++++++++ internal/syncer/syncer.go | 1 + internal/syncer/syncer_test.go | 10 ++++ 7 files changed, 129 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e5a49..7f0d806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,12 @@ - Add Developer ID signing and release verification for official macOS binaries under OpenClaw Foundation Team ID `FWJYW4S8P8`, while keeping local and cross-platform snapshot builds credential-free. - Update CrawlKit to v0.13.4 and Go to 1.26.5, including the standard-library `crypto/tls` security fixes. +### Fixes + +- Resolve targeted channel/thread sync requests directly before falling back to + broad thread catalog crawls, so archived thread syncs do not wait on unrelated + guild thread endpoints. + ## 0.11.4 - 2026-07-02 ### Changes diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 0cb540b..08167fe 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -3616,6 +3616,10 @@ func (f *fakeDiscordClient) Guild(context.Context, string) (*discordgo.Guild, er return &discordgo.Guild{}, nil } +func (f *fakeDiscordClient) Channel(context.Context, string) (*discordgo.Channel, error) { + return nil, nil +} + func (f *fakeDiscordClient) GuildChannels(context.Context, string) ([]*discordgo.Channel, error) { return nil, nil } diff --git a/internal/discord/client.go b/internal/discord/client.go index 0fef474..4d75a06 100644 --- a/internal/discord/client.go +++ b/internal/discord/client.go @@ -293,6 +293,12 @@ func (c *Client) Guild(ctx context.Context, guildID string) (*discordgo.Guild, e return c.session.Guild(guildID, discordgo.WithContext(reqCtx)) } +func (c *Client) Channel(ctx context.Context, channelID string) (*discordgo.Channel, error) { + reqCtx, cancel := c.requestContext(ctx) + defer cancel() + return c.session.Channel(channelID, discordgo.WithContext(reqCtx)) +} + func (c *Client) GuildChannels(ctx context.Context, guildID string) ([]*discordgo.Channel, error) { reqCtx, cancel := c.requestContext(ctx) defer cancel() diff --git a/internal/syncer/channel_catalog.go b/internal/syncer/channel_catalog.go index acc48b8..ba80ba7 100644 --- a/internal/syncer/channel_catalog.go +++ b/internal/syncer/channel_catalog.go @@ -62,6 +62,13 @@ func (s *Syncer) channelList(ctx context.Context, guildID string, requested []st selected = selectRequestedChannels(allChannels, storedByID, requestedSet) } + if unresolvedRequestedIDs(selected, requestedSet) > 0 { + if err := s.appendDirectRequestedChannels(ctx, guildID, allChannels, requestedSet); err != nil { + return nil, false, err + } + selected = selectRequestedChannels(allChannels, storedByID, requestedSet) + } + if unresolvedRequestedIDs(selected, requestedSet) > 0 { if err := s.appendThreadCatalog(ctx, allChannels, threadParentIDs(topLevel)); err != nil { return nil, false, err @@ -75,6 +82,31 @@ func (s *Syncer) channelList(ctx context.Context, guildID string, requested []st ), true, nil } +func (s *Syncer) appendDirectRequestedChannels( + ctx context.Context, + guildID string, + allChannels map[string]*discordgo.Channel, + requested map[string]struct{}, +) error { + for requestedID := range requested { + if _, ok := allChannels[requestedID]; ok { + continue + } + channel, err := s.client.Channel(ctx, requestedID) + if err != nil { + return fmt.Errorf("fetch requested channel %s: %w", requestedID, err) + } + if channel == nil || channel.ID == "" { + continue + } + if channel.GuildID != "" && guildID != "" && channel.GuildID != guildID { + return fmt.Errorf("requested channel %s belongs to guild %s, not %s", channel.ID, channel.GuildID, guildID) + } + allChannels[channel.ID] = channel + } + return nil +} + func (s *Syncer) liveChannelList(ctx context.Context, guildID string, mode channelCatalogMode, exclusions channelExclusions) ([]*discordgo.Channel, error) { channels, err := s.client.GuildChannels(ctx, guildID) if err != nil { diff --git a/internal/syncer/channel_catalog_test.go b/internal/syncer/channel_catalog_test.go index 1272251..3e8da22 100644 --- a/internal/syncer/channel_catalog_test.go +++ b/internal/syncer/channel_catalog_test.go @@ -123,6 +123,76 @@ func TestSyncChannelSubsetExpandsRequestedForumThreads(t *testing.T) { require.Equal(t, "t1", results[0].ChannelID) } +func TestSyncChannelSubsetFetchesRequestedArchivedThreadDirectly(t *testing.T) { + t.Parallel() + + ctx := context.Background() + s, err := store.Open(ctx, filepath.Join(t.TempDir(), "discrawl.db")) + require.NoError(t, err) + defer func() { _ = s.Close() }() + + require.NoError(t, s.UpsertGuild(ctx, store.GuildRecord{ID: "g1", Name: "Guild", RawJSON: `{}`})) + + archiveAt := time.Now().UTC().Add(-time.Hour) + client := &fakeClient{ + guilds: []*discordgo.UserGuild{{ID: "g1", Name: "Guild"}}, + guildByID: map[string]*discordgo.Guild{ + "g1": {ID: "g1", Name: "Guild"}, + }, + channels: map[string][]*discordgo.Channel{ + "g1": { + {ID: "f1", GuildID: "g1", Name: "support", Type: discordgo.ChannelTypeGuildForum}, + {ID: "c2", GuildID: "g1", Name: "random", Type: discordgo.ChannelTypeGuildText}, + }, + }, + channelByID: map[string]*discordgo.Channel{ + "t-archived": { + ID: "t-archived", + GuildID: "g1", + ParentID: "f1", + Name: "archived support thread", + Type: discordgo.ChannelTypeGuildPublicThread, + ThreadMetadata: &discordgo.ThreadMetadata{ + Archived: true, + ArchiveTimestamp: archiveAt, + }, + LastMessageID: "10", + }, + }, + messages: map[string][]*discordgo.Message{ + "t-archived": {{ + ID: "10", + GuildID: "g1", + ChannelID: "t-archived", + Content: "archived support body", + Timestamp: archiveAt, + Author: &discordgo.User{ID: "u1", Username: "user"}, + }}, + }, + } + + svc := New(client, s, nil) + stats, err := svc.Sync(ctx, SyncOptions{ + Full: true, + GuildIDs: []string{"g1"}, + ChannelIDs: []string{"t-archived"}, + }) + require.NoError(t, err) + require.Equal(t, 1, stats.Channels) + require.Equal(t, 1, stats.Threads) + require.Equal(t, 1, stats.Messages) + require.Equal(t, 1, client.guildChanCalls) + require.Equal(t, 1, client.channelCalls["t-archived"]) + require.Zero(t, client.threadCalls) + require.Empty(t, client.archivedCalls) + require.Equal(t, 1, client.messageCalls["t-archived"]) + + results, err := s.SearchMessages(ctx, store.SearchOptions{Query: "archived support"}) + require.NoError(t, err) + require.Len(t, results, 1) + require.Equal(t, "t-archived", results[0].ChannelID) +} + func TestSyncToleratesArchivedThread403(t *testing.T) { t.Parallel() diff --git a/internal/syncer/syncer.go b/internal/syncer/syncer.go index 5fd9e14..22c7bdc 100644 --- a/internal/syncer/syncer.go +++ b/internal/syncer/syncer.go @@ -18,6 +18,7 @@ type Client interface { Self(context.Context) (*discordgo.User, error) Guilds(context.Context) ([]*discordgo.UserGuild, error) Guild(context.Context, string) (*discordgo.Guild, error) + Channel(context.Context, string) (*discordgo.Channel, error) GuildChannels(context.Context, string) ([]*discordgo.Channel, error) ThreadsActive(context.Context, string) ([]*discordgo.Channel, error) GuildThreadsActive(context.Context, string) ([]*discordgo.Channel, error) diff --git a/internal/syncer/syncer_test.go b/internal/syncer/syncer_test.go index ac6e4ce..0520449 100644 --- a/internal/syncer/syncer_test.go +++ b/internal/syncer/syncer_test.go @@ -18,6 +18,7 @@ import ( type fakeClient struct { guilds []*discordgo.UserGuild guildByID map[string]*discordgo.Guild + channelByID map[string]*discordgo.Channel channels map[string][]*discordgo.Channel activeThreads map[string][]*discordgo.Channel guildThreads map[string][]*discordgo.Channel @@ -41,6 +42,7 @@ type fakeClient struct { tailCalls int tailHandled chan struct{} messageDelay time.Duration + channelCalls map[string]int guildChanCalls int threadCalls int guildThreadCalls int @@ -68,6 +70,14 @@ func (f *fakeClient) Guild(_ context.Context, guildID string) (*discordgo.Guild, return f.guildByID[guildID], nil } +func (f *fakeClient) Channel(_ context.Context, channelID string) (*discordgo.Channel, error) { + if f.channelCalls == nil { + f.channelCalls = make(map[string]int) + } + f.channelCalls[channelID]++ + return f.channelByID[channelID], nil +} + func (f *fakeClient) GuildChannels(_ context.Context, guildID string) ([]*discordgo.Channel, error) { f.guildChanCalls++ return f.channels[guildID], nil From b765c6a7bd07808e35df09b14dc7025f0cc66e9d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Jul 2026 22:05:25 -0700 Subject: [PATCH 2/4] chore: remove contributor changelog entry --- CHANGELOG.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f0d806..03e5a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,12 +71,6 @@ - Add Developer ID signing and release verification for official macOS binaries under OpenClaw Foundation Team ID `FWJYW4S8P8`, while keeping local and cross-platform snapshot builds credential-free. - Update CrawlKit to v0.13.4 and Go to 1.26.5, including the standard-library `crypto/tls` security fixes. -### Fixes - -- Resolve targeted channel/thread sync requests directly before falling back to - broad thread catalog crawls, so archived thread syncs do not wait on unrelated - guild thread endpoints. - ## 0.11.4 - 2026-07-02 ### Changes From f56e188a4abb12d4a1cdbc2da126e1535d5eda64 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Jul 2026 22:12:36 -0700 Subject: [PATCH 3/4] fix: handle targeted channels across selected guilds --- internal/syncer/channel_catalog.go | 14 +++++++++--- internal/syncer/channel_catalog_test.go | 25 +++++++++++++++++----- internal/syncer/channel_exclusions_test.go | 1 + internal/syncer/syncer.go | 7 +++++- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/internal/syncer/channel_catalog.go b/internal/syncer/channel_catalog.go index ba80ba7..13c9107 100644 --- a/internal/syncer/channel_catalog.go +++ b/internal/syncer/channel_catalog.go @@ -19,7 +19,7 @@ const ( channelCatalogIncremental ) -func (s *Syncer) channelList(ctx context.Context, guildID string, requested []string, mode channelCatalogMode, exclusions channelExclusions) ([]*discordgo.Channel, bool, error) { +func (s *Syncer) channelList(ctx context.Context, guildID string, requested []string, mode channelCatalogMode, exclusions channelExclusions, selectedGuildIDs map[string]struct{}) ([]*discordgo.Channel, bool, error) { if len(requested) == 0 { channels, err := s.liveChannelList(ctx, guildID, mode, exclusions) if err != nil { @@ -63,7 +63,7 @@ func (s *Syncer) channelList(ctx context.Context, guildID string, requested []st } if unresolvedRequestedIDs(selected, requestedSet) > 0 { - if err := s.appendDirectRequestedChannels(ctx, guildID, allChannels, requestedSet); err != nil { + if err := s.appendDirectRequestedChannels(ctx, guildID, allChannels, requestedSet, selectedGuildIDs); err != nil { return nil, false, err } selected = selectRequestedChannels(allChannels, storedByID, requestedSet) @@ -87,6 +87,7 @@ func (s *Syncer) appendDirectRequestedChannels( guildID string, allChannels map[string]*discordgo.Channel, requested map[string]struct{}, + selectedGuildIDs map[string]struct{}, ) error { for requestedID := range requested { if _, ok := allChannels[requestedID]; ok { @@ -100,7 +101,14 @@ func (s *Syncer) appendDirectRequestedChannels( continue } if channel.GuildID != "" && guildID != "" && channel.GuildID != guildID { - return fmt.Errorf("requested channel %s belongs to guild %s, not %s", channel.ID, channel.GuildID, guildID) + if _, selected := selectedGuildIDs[channel.GuildID]; !selected { + return fmt.Errorf("requested channel %s belongs to guild %s, not %s", channel.ID, channel.GuildID, guildID) + } + // requested is rebuilt by channelList for each selected guild. Exclude + // the target only from this guild's fallback discovery; its owning guild + // gets a fresh request set and performs the same direct lookup. + delete(requested, requestedID) + continue } allChannels[channel.ID] = channel } diff --git a/internal/syncer/channel_catalog_test.go b/internal/syncer/channel_catalog_test.go index 3e8da22..16485a3 100644 --- a/internal/syncer/channel_catalog_test.go +++ b/internal/syncer/channel_catalog_test.go @@ -135,11 +135,18 @@ func TestSyncChannelSubsetFetchesRequestedArchivedThreadDirectly(t *testing.T) { archiveAt := time.Now().UTC().Add(-time.Hour) client := &fakeClient{ - guilds: []*discordgo.UserGuild{{ID: "g1", Name: "Guild"}}, + guilds: []*discordgo.UserGuild{ + {ID: "g-other", Name: "Other Guild"}, + {ID: "g1", Name: "Guild"}, + }, guildByID: map[string]*discordgo.Guild{ - "g1": {ID: "g1", Name: "Guild"}, + "g-other": {ID: "g-other", Name: "Other Guild"}, + "g1": {ID: "g1", Name: "Guild"}, }, channels: map[string][]*discordgo.Channel{ + "g-other": { + {ID: "f-other", GuildID: "g-other", Name: "other forum", Type: discordgo.ChannelTypeGuildForum}, + }, "g1": { {ID: "f1", GuildID: "g1", Name: "support", Type: discordgo.ChannelTypeGuildForum}, {ID: "c2", GuildID: "g1", Name: "random", Type: discordgo.ChannelTypeGuildText}, @@ -174,15 +181,16 @@ func TestSyncChannelSubsetFetchesRequestedArchivedThreadDirectly(t *testing.T) { svc := New(client, s, nil) stats, err := svc.Sync(ctx, SyncOptions{ Full: true, - GuildIDs: []string{"g1"}, + GuildIDs: []string{"g-other", "g1"}, ChannelIDs: []string{"t-archived"}, }) require.NoError(t, err) + require.Equal(t, 2, stats.Guilds) require.Equal(t, 1, stats.Channels) require.Equal(t, 1, stats.Threads) require.Equal(t, 1, stats.Messages) - require.Equal(t, 1, client.guildChanCalls) - require.Equal(t, 1, client.channelCalls["t-archived"]) + require.Equal(t, 2, client.guildChanCalls) + require.Equal(t, 2, client.channelCalls["t-archived"]) require.Zero(t, client.threadCalls) require.Empty(t, client.archivedCalls) require.Equal(t, 1, client.messageCalls["t-archived"]) @@ -191,6 +199,13 @@ func TestSyncChannelSubsetFetchesRequestedArchivedThreadDirectly(t *testing.T) { require.NoError(t, err) require.Len(t, results, 1) require.Equal(t, "t-archived", results[0].ChannelID) + + _, err = svc.Sync(ctx, SyncOptions{ + Full: true, + GuildIDs: []string{"g-other"}, + ChannelIDs: []string{"t-archived"}, + }) + require.ErrorContains(t, err, "requested channel t-archived belongs to guild g1, not g-other") } func TestSyncToleratesArchivedThread403(t *testing.T) { diff --git a/internal/syncer/channel_exclusions_test.go b/internal/syncer/channel_exclusions_test.go index 3418a4d..a7c8638 100644 --- a/internal/syncer/channel_exclusions_test.go +++ b/internal/syncer/channel_exclusions_test.go @@ -136,6 +136,7 @@ func TestTargetedStoredThreadUsesFullCategoryAncestry(t *testing.T) { []string{"thread-a"}, channelCatalogFull, svc.effectiveChannelExclusions(SyncOptions{}), + map[string]struct{}{"g1": {}}, ) require.NoError(t, err) require.True(t, targeted) diff --git a/internal/syncer/syncer.go b/internal/syncer/syncer.go index 22c7bdc..1e0b58d 100644 --- a/internal/syncer/syncer.go +++ b/internal/syncer/syncer.go @@ -66,6 +66,7 @@ type SyncOptions struct { ExcludeChannelKinds []string IncludeCategoryIDs []string RepairReason string + selectedGuildIDs map[string]struct{} } func (s *Syncer) SetTailReadyCallback(fn func(context.Context) error) { @@ -155,6 +156,10 @@ func (s *Syncer) Sync(ctx context.Context, opts SyncOptions) (SyncStats, error) return SyncStats{}, fmt.Errorf("requested guilds not accessible: %s", strings.Join(missing, ", ")) } targets := selectGuilds(guilds, opts.GuildIDs) + opts.selectedGuildIDs = make(map[string]struct{}, len(targets)) + for _, guild := range targets { + opts.selectedGuildIDs[guild.ID] = struct{}{} + } stats := SyncStats{} for _, guild := range targets { one, err := s.syncGuild(ctx, guild.ID, opts) @@ -199,7 +204,7 @@ func (s *Syncer) syncGuild(ctx context.Context, guildID string, opts SyncOptions } } exclusions := s.effectiveChannelExclusions(opts) - channelList, targeted, err := s.channelList(ctx, guildID, opts.ChannelIDs, catalogMode, exclusions) + channelList, targeted, err := s.channelList(ctx, guildID, opts.ChannelIDs, catalogMode, exclusions, opts.selectedGuildIDs) if err != nil { return stats, err } From 1ff839d1802bf644f8db4cf24659a7a9e8cf762d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Jul 2026 22:18:07 -0700 Subject: [PATCH 4/4] fix: cache targeted channel lookups --- internal/syncer/channel_catalog.go | 37 ++++++++++++++++---- internal/syncer/channel_catalog_test.go | 10 +++++- internal/syncer/channel_exclusions_test.go | 1 + internal/syncer/syncer.go | 40 ++++++++++++++-------- 4 files changed, 66 insertions(+), 22 deletions(-) diff --git a/internal/syncer/channel_catalog.go b/internal/syncer/channel_catalog.go index 13c9107..f75adc4 100644 --- a/internal/syncer/channel_catalog.go +++ b/internal/syncer/channel_catalog.go @@ -14,12 +14,25 @@ import ( type channelCatalogMode int +type directChannelResult struct { + channel *discordgo.Channel + err error +} + const ( channelCatalogFull channelCatalogMode = iota channelCatalogIncremental ) -func (s *Syncer) channelList(ctx context.Context, guildID string, requested []string, mode channelCatalogMode, exclusions channelExclusions, selectedGuildIDs map[string]struct{}) ([]*discordgo.Channel, bool, error) { +func (s *Syncer) channelList( + ctx context.Context, + guildID string, + requested []string, + mode channelCatalogMode, + exclusions channelExclusions, + selectedGuildIDs map[string]struct{}, + directChannelResults map[string]directChannelResult, +) ([]*discordgo.Channel, bool, error) { if len(requested) == 0 { channels, err := s.liveChannelList(ctx, guildID, mode, exclusions) if err != nil { @@ -63,7 +76,10 @@ func (s *Syncer) channelList(ctx context.Context, guildID string, requested []st } if unresolvedRequestedIDs(selected, requestedSet) > 0 { - if err := s.appendDirectRequestedChannels(ctx, guildID, allChannels, requestedSet, selectedGuildIDs); err != nil { + if directChannelResults == nil { + directChannelResults = make(map[string]directChannelResult, len(requestedSet)) + } + if err := s.appendDirectRequestedChannels(ctx, guildID, allChannels, requestedSet, selectedGuildIDs, directChannelResults); err != nil { return nil, false, err } selected = selectRequestedChannels(allChannels, storedByID, requestedSet) @@ -88,19 +104,28 @@ func (s *Syncer) appendDirectRequestedChannels( allChannels map[string]*discordgo.Channel, requested map[string]struct{}, selectedGuildIDs map[string]struct{}, + directChannelResults map[string]directChannelResult, ) error { for requestedID := range requested { if _, ok := allChannels[requestedID]; ok { continue } - channel, err := s.client.Channel(ctx, requestedID) - if err != nil { - return fmt.Errorf("fetch requested channel %s: %w", requestedID, err) + result, cached := directChannelResults[requestedID] + if !cached { + result.channel, result.err = s.client.Channel(ctx, requestedID) + directChannelResults[requestedID] = result } + if result.err != nil { + return fmt.Errorf("fetch requested channel %s: %w", requestedID, result.err) + } + channel := result.channel if channel == nil || channel.ID == "" { continue } - if channel.GuildID != "" && guildID != "" && channel.GuildID != guildID { + if channel.GuildID == "" { + return fmt.Errorf("requested channel %s is not a guild channel", channel.ID) + } + if channel.GuildID != guildID { if _, selected := selectedGuildIDs[channel.GuildID]; !selected { return fmt.Errorf("requested channel %s belongs to guild %s, not %s", channel.ID, channel.GuildID, guildID) } diff --git a/internal/syncer/channel_catalog_test.go b/internal/syncer/channel_catalog_test.go index 16485a3..2afe012 100644 --- a/internal/syncer/channel_catalog_test.go +++ b/internal/syncer/channel_catalog_test.go @@ -190,7 +190,7 @@ func TestSyncChannelSubsetFetchesRequestedArchivedThreadDirectly(t *testing.T) { require.Equal(t, 1, stats.Threads) require.Equal(t, 1, stats.Messages) require.Equal(t, 2, client.guildChanCalls) - require.Equal(t, 2, client.channelCalls["t-archived"]) + require.Equal(t, 1, client.channelCalls["t-archived"]) require.Zero(t, client.threadCalls) require.Empty(t, client.archivedCalls) require.Equal(t, 1, client.messageCalls["t-archived"]) @@ -206,6 +206,14 @@ func TestSyncChannelSubsetFetchesRequestedArchivedThreadDirectly(t *testing.T) { ChannelIDs: []string{"t-archived"}, }) require.ErrorContains(t, err, "requested channel t-archived belongs to guild g1, not g-other") + + client.channelByID["dm"] = &discordgo.Channel{ID: "dm", Type: discordgo.ChannelTypeDM} + _, err = svc.Sync(ctx, SyncOptions{ + Full: true, + GuildIDs: []string{"g1"}, + ChannelIDs: []string{"dm"}, + }) + require.ErrorContains(t, err, "requested channel dm is not a guild channel") } func TestSyncToleratesArchivedThread403(t *testing.T) { diff --git a/internal/syncer/channel_exclusions_test.go b/internal/syncer/channel_exclusions_test.go index a7c8638..e3128c2 100644 --- a/internal/syncer/channel_exclusions_test.go +++ b/internal/syncer/channel_exclusions_test.go @@ -137,6 +137,7 @@ func TestTargetedStoredThreadUsesFullCategoryAncestry(t *testing.T) { channelCatalogFull, svc.effectiveChannelExclusions(SyncOptions{}), map[string]struct{}{"g1": {}}, + nil, ) require.NoError(t, err) require.True(t, targeted) diff --git a/internal/syncer/syncer.go b/internal/syncer/syncer.go index 1e0b58d..853ad0c 100644 --- a/internal/syncer/syncer.go +++ b/internal/syncer/syncer.go @@ -53,20 +53,21 @@ type Syncer struct { } type SyncOptions struct { - Full bool - GuildIDs []string - ChannelIDs []string - Concurrency int - Since time.Time - Embeddings bool - SkipMembers bool - RequireMembers bool - LatestOnly bool - ExcludeChannelIDs []string - ExcludeChannelKinds []string - IncludeCategoryIDs []string - RepairReason string - selectedGuildIDs map[string]struct{} + Full bool + GuildIDs []string + ChannelIDs []string + Concurrency int + Since time.Time + Embeddings bool + SkipMembers bool + RequireMembers bool + LatestOnly bool + ExcludeChannelIDs []string + ExcludeChannelKinds []string + IncludeCategoryIDs []string + RepairReason string + selectedGuildIDs map[string]struct{} + directChannelResults map[string]directChannelResult } func (s *Syncer) SetTailReadyCallback(fn func(context.Context) error) { @@ -157,6 +158,7 @@ func (s *Syncer) Sync(ctx context.Context, opts SyncOptions) (SyncStats, error) } targets := selectGuilds(guilds, opts.GuildIDs) opts.selectedGuildIDs = make(map[string]struct{}, len(targets)) + opts.directChannelResults = make(map[string]directChannelResult, len(opts.ChannelIDs)) for _, guild := range targets { opts.selectedGuildIDs[guild.ID] = struct{}{} } @@ -204,7 +206,15 @@ func (s *Syncer) syncGuild(ctx context.Context, guildID string, opts SyncOptions } } exclusions := s.effectiveChannelExclusions(opts) - channelList, targeted, err := s.channelList(ctx, guildID, opts.ChannelIDs, catalogMode, exclusions, opts.selectedGuildIDs) + channelList, targeted, err := s.channelList( + ctx, + guildID, + opts.ChannelIDs, + catalogMode, + exclusions, + opts.selectedGuildIDs, + opts.directChannelResults, + ) if err != nil { return stats, err }