fix(factory): resolve Linear states without pinned IDs#45
Conversation
|
Warning Review limit reached
More reviews will be available in 51 minutes and 8 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements a fallback mechanism to reconstruct the Linear state catalog from synced issue files when the states catalog is absent, updating the reader interface, tests, and cloud mount scopes accordingly. The reviewer feedback highlights a potential performance bottleneck when scanning all issue files sequentially, recommending capping the scan limit and stopping early once target states are found. Additionally, the reviewer notes a regression where misspelled optional states are silently ignored, suggesting tracking whether the catalog is a fallback to safely handle missing optional states without swallowing configuration errors.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| async recordsFromIssues(): Promise<StateRecord[]> { | ||
| if (!this.reader.listTree) return [] | ||
| let paths: string[] | ||
| try { | ||
| paths = await this.reader.listTree('/linear/issues') | ||
| } catch { | ||
| return [] | ||
| } | ||
|
|
||
| const byKey = new Map<string, StateRecord>() | ||
| const issuePaths = paths.filter((path) => | ||
| path.endsWith('.json') && | ||
| !path.includes('/comments/') && | ||
| !path.startsWith('/linear/issues/by-uuid/'), | ||
| ) | ||
| for (let i = 0; i < issuePaths.length; i += CATALOG_READ_CONCURRENCY) { | ||
| const batch = await Promise.all( | ||
| issuePaths.slice(i, i + CATALOG_READ_CONCURRENCY).map(async (path): Promise<StateRecord | undefined> => { | ||
| try { | ||
| const payload = unwrap((await this.reader.readFile(path)).content) | ||
| const state = asRecord(payload.state) | ||
| const id = str(payload.stateId) ?? str(state?.id) | ||
| const name = str(state?.name) | ||
| if (!id || !name) return undefined | ||
| return { | ||
| id, | ||
| name, | ||
| teamKey: str(asRecord(payload.team)?.key) ?? str(payload.teamKey), | ||
| teamName: str(asRecord(payload.team)?.name) ?? str(payload.teamName), | ||
| } | ||
| } catch { | ||
| return undefined | ||
| } | ||
| }), | ||
| ) | ||
| for (const record of batch) { | ||
| if (!record) continue | ||
| byKey.set(`${norm(record.teamKey) || norm(record.teamName)}:${norm(record.name)}:${record.id}`, record) | ||
| } | ||
| } | ||
| return [...byKey.values()] | ||
| } |
There was a problem hiding this comment.
Performance Bottleneck in Issue Fallback Scanning
Reading every single issue file sequentially in batches of 10 over a remote/cloud mount can cause severe performance degradation, startup timeouts, or rate limits if the workspace contains thousands of issues.
To optimize this, we can:
- Pass the set of configured target state names to
StateCatalogand stop scanning issues early as soon as we have found a state ID for every target name. - Cap the maximum number of issues scanned (e.g., 150) to prevent scanning the entire history if a configured state is never used in any issue.
async recordsFromIssues(targetNames?: Set<string>): Promise<StateRecord[]> {
if (!this.reader.listTree) return []
let paths: string[]
try {
paths = await this.reader.listTree('/linear/issues')
} catch {
return []
}
const byKey = new Map<string, StateRecord>()
const issuePaths = paths.filter((path) =>
path.endsWith('.json') &&
!path.includes('/comments/') &&
!path.startsWith('/linear/issues/by-uuid/'),
)
const foundNames = new Set<string>()
const maxIssuesToScan = 150
for (let i = 0; i < issuePaths.length && i < maxIssuesToScan; i += CATALOG_READ_CONCURRENCY) {
if (targetNames && targetNames.size > 0 && foundNames.size >= targetNames.size) {
break
}
const batch = await Promise.all(
issuePaths.slice(i, i + CATALOG_READ_CONCURRENCY).map(async (path): Promise<StateRecord | undefined> => {
try {
const payload = unwrap((await this.reader.readFile(path)).content)
const state = asRecord(payload.state)
const id = str(payload.stateId) ?? str(state?.id)
const name = str(state?.name)
if (!id || !name) return undefined
return {
id,
name,
teamKey: str(asRecord(payload.team)?.key) ?? str(payload.teamKey),
teamName: str(asRecord(payload.team)?.name) ?? str(payload.teamName),
}
} catch {
return undefined
}
}),
)
for (const record of batch) {
if (!record) continue
byKey.set(norm(record.teamKey) || norm(record.teamName) + ":" + norm(record.name) + ":" + record.id, record)
if (record.name) {
foundNames.add(norm(record.name))
}
}
}
return [...byKey.values()]
}| if (!REQUIRED_ROLES.includes(role) && !/ambiguous/u.test(error instanceof Error ? error.message : String(error))) { | ||
| return undefined | ||
| } |
There was a problem hiding this comment.
Regression: Misspelled Optional States Silently Ignored
Using !/ambiguous/u.test(...) to catch non-ambiguous errors will silently return undefined even when the catalog is healthy but the optional state name is misspelled (e.g., a typo in humanReview). This silently disables the optional role instead of failing startup with a clear error.
We should only tolerate missing optional states if the catalog is completely unavailable (CatalogUnavailableError) or if we had to fall back to reconstructing it from issues (catalog.isFallback).
if (!REQUIRED_ROLES.includes(role)) {
if (error instanceof CatalogUnavailableError) {
return undefined
}
if (catalog.isFallback && !/ambiguous/u.test(error instanceof Error ? error.message : String(error))) {
return undefined
}
}| class StateCatalog { | ||
| #records: StateRecord[] | undefined | ||
| constructor(private readonly reader: LinearStateReader) {} |
There was a problem hiding this comment.
Track Fallback State
We need to track whether the catalog was loaded from the official /linear/states/_index.json or reconstructed from issues, so we can safely decide whether to tolerate missing optional states.
class StateCatalog {
#records: StateRecord[] | undefined
isFallback = false
constructor(
private readonly reader: LinearStateReader,
private readonly targetNames?: Set<string>,
) {}| const issueRecords = await this.recordsFromIssues() | ||
| if (issueRecords.length > 0) { | ||
| this.#records = issueRecords | ||
| return issueRecords | ||
| } |
There was a problem hiding this comment.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2fa7df1702
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const payload = unwrap((await this.reader.readFile(path)).content) | ||
| const state = asRecord(payload.state) | ||
| const id = str(payload.stateId) ?? str(state?.id) | ||
| const name = str(state?.name) |
There was a problem hiding this comment.
Preserve top-level state_name in issue fallback
When /linear/states/_index.json is absent and the fallback harvests state records from /linear/issues, synced issues that carry stateId/state.id plus top-level state_name are ignored because name only reads state.name. That sparse shape is already accepted by parseLinearIssue, so these mounts still fail startup with missing roles even though each issue contains the state id/name needed for resolution; include payload.state_name as a fallback here.
Useful? React with 👍 / 👎.
| // An OPTIONAL role (e.g. humanReview) tolerates an absent or incomplete | ||
| // catalog. Ambiguity still surfaces because silently picking one of many | ||
| // matching optional states would be a real misconfiguration. | ||
| if (!REQUIRED_ROLES.includes(role) && !/ambiguous/u.test(error instanceof Error ? error.message : String(error))) { |
There was a problem hiding this comment.
Keep missing human-review states loud
In a workspace where /linear/states is readable but the configured humanReview name is misspelled or absent for a subscribed team, catalog.resolve() throws No Linear workflow state...; this new predicate treats that the same as an incomplete fallback catalog and returns undefined. Because hasHumanReview() then becomes false, terminalState: human-review silently falls back to Done instead of surfacing the misconfiguration; the previous CatalogUnavailableError guard only tolerated an unreadable catalog.
Useful? React with 👍 / 👎.
|
ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed. Review: PR #45 —
|
2fa7df1 to
8ff01e3
Compare
|
Addressed the review feedback in 8ff01e3:
Verification:
|
Review: PR #45 —
|
Summary
Verification
Summary by cubic
Resolve Linear state names without pinned IDs. Prefer the /linear/states catalog, and fall back to synced /linear/issues data when it's not readable, with bounded scanning.
Written for commit 8ff01e3. Summary will update on new commits.