@@ -285,6 +285,7 @@ public enum Reducer {
285285 }
286286 link. tmuxLink = TmuxLink ( sessionName: tmuxName, extraSessions: extras. isEmpty ? nil : extras)
287287 link. column = . inProgress
288+ link. manualOverrides. column = false // Let automatic assignment take over
288289 link. isLaunching = true
289290 link. updatedAt = . now
290291 state. links [ cardId] = link
@@ -303,6 +304,7 @@ public enum Reducer {
303304 }
304305 link. tmuxLink = TmuxLink ( sessionName: tmuxName, extraSessions: extras. isEmpty ? nil : extras)
305306 link. column = . inProgress
307+ link. manualOverrides. column = false // Let automatic assignment take over
306308 link. isLaunching = true
307309 link. updatedAt = . now
308310 state. links [ cardId] = link
@@ -882,6 +884,7 @@ public final class BoardStore: @unchecked Sendable {
882884 // Dependencies for reconciliation
883885 private var isReconciling = false
884886 private var lastGHLookup : ContinuousClock . Instant = . now - . seconds( 600 )
887+ private var ghRateLimitedUntil : ContinuousClock . Instant = . now
885888 public var appIsActive : Bool = true
886889 private let discovery : SessionDiscovery
887890 private let coordinationStore : CoordinationStore
@@ -1082,15 +1085,16 @@ public final class BoardStore: @unchecked Sendable {
10821085 }
10831086
10841087 // Fetch PR data via targeted GraphQL — concurrent across repos (max 5)
1085- // Throttle: every reconcile cycle when active, every 5min when backgrounded/hidden.
1086- let ghInterval : Duration = appIsActive ? . seconds( 0 ) : . seconds( 300 )
1088+ // Throttle: 30s when active, 5min when backgrounded/hidden, 5min after rate limit.
1089+ let ghInterval : Duration = ghRateLimitedUntil > . now ? . seconds( 300 )
1090+ : appIsActive ? . seconds( 30 ) : . seconds( 300 )
10871091 let shouldFetchPRs = ContinuousClock . now - lastGHLookup >= ghInterval
10881092 var pullRequests : [ String : PullRequest ] = [ : ] // branch → PR for reconciler
10891093 var prsByRepoAndNumber : [ String : [ Int : PullRequest ] ] = [ : ] // repo → number → PR
10901094 if let ghAdapter, shouldFetchPRs {
10911095 let t = ContinuousClock . now
10921096 let allRepos = Set ( branchesByRepo. keys) . union ( prNumbersByRepo. keys)
1093- typealias PRResult = ( String , [ String : PullRequest ] , [ Int : PullRequest ] )
1097+ typealias PRResult = ( String , [ String : PullRequest ] , [ Int : PullRequest ] , Bool )
10941098 let results : [ PRResult ] = await withTaskGroup ( of: PRResult . self) { group in
10951099 var pending = 0
10961100 var collected : [ PRResult ] = [ ]
@@ -1107,27 +1111,39 @@ public final class BoardStore: @unchecked Sendable {
11071111
11081112 group. addTask {
11091113 let tBatch = ContinuousClock . now
1110- let ( byBranch, byNumber) = ( try ? await ghAdapter. batchPRLookup (
1111- repoRoot: repoRoot, branches: branches, prNumbers: numbers
1112- ) ) ?? ( [ : ] , [ : ] )
1113- let repoName = ( repoRoot as NSString ) . lastPathComponent
1114- KanbanCodeLog . info ( " reconcile " , " batchPRLookup( \( repoName) ): \( tBatch. duration ( to: . now) ) ( \( branches. count) branches, \( numbers. count) PRs) " )
1115- return ( repoRoot, byBranch, byNumber)
1114+ do {
1115+ let ( byBranch, byNumber) = try await ghAdapter. batchPRLookup (
1116+ repoRoot: repoRoot, branches: branches, prNumbers: numbers
1117+ )
1118+ let repoName = ( repoRoot as NSString ) . lastPathComponent
1119+ KanbanCodeLog . info ( " reconcile " , " batchPRLookup( \( repoName) ): \( tBatch. duration ( to: . now) ) ( \( branches. count) branches, \( numbers. count) PRs) " )
1120+ return ( repoRoot, byBranch, byNumber, false )
1121+ } catch is GhCliError {
1122+ return ( repoRoot, [ : ] , [ : ] , true )
1123+ } catch {
1124+ return ( repoRoot, [ : ] , [ : ] , false )
1125+ }
11161126 }
11171127 pending += 1
11181128 }
11191129 for await result in group { collected. append ( result) }
11201130 return collected
11211131 }
11221132
1123- for (repoRoot, byBranch, byNumber) in results {
1133+ var hitRateLimit = false
1134+ for (repoRoot, byBranch, byNumber, rateLimited) in results {
1135+ if rateLimited { hitRateLimit = true }
11241136 for (branch, pr) in byBranch {
11251137 pullRequests [ branch] = pr
11261138 }
11271139 if !byNumber. isEmpty {
11281140 prsByRepoAndNumber [ repoRoot] = byNumber
11291141 }
11301142 }
1143+ if hitRateLimit {
1144+ ghRateLimitedUntil = . now + . seconds( 300 )
1145+ dispatch ( . setError( " GitHub API rate limit exceeded — pausing PR lookups for 5 minutes " ) )
1146+ }
11311147 let totalByNumber = prsByRepoAndNumber. values. reduce ( 0 ) { $0 + $1. count }
11321148 KanbanCodeLog . info ( " reconcile " , " PR lookup: \( t. duration ( to: . now) ) ( \( pullRequests. count) by branch, \( totalByNumber) by number, \( allRepos. count) repos) " )
11331149 lastGHLookup = . now
0 commit comments