You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[bug] Linear changes dropped during the #7317 cutover were never recovered, so tasks stay stale until edited again #7739
Linear issue changes made during the 19 September window between #7317 reaching
production and the #7675 backfill never reached tasks. The affected tasks keep
the state they had just before the window and never heal on their own: the
Tasks board still shows them in their old column three days later.
Our case: six Linear issues moved to Done between 21:06 and 21:13 UTC on
2026-09-19. Their earlier moves to In Progress (20:11–20:30 UTC the same
evening) synced. Their Superset rows still read the old status with externalUpdatedAt of 2026-09-19 20:29–20:35 UTC and completedAt: null. The
Linear "Done" workflow state is in task_statuses (synced 2026-09-01), so this
is not #5216.
webhook/route.ts checks hasActiveSubscriber before recordWebhookDelivery,
so the dropped deliveries were never written to ingest.webhook_events and
cannot be replayed from there.
Nothing re-reads Linear afterwards: task_statuses and tasks are only pulled
by jobs/initial-sync on connect. The only repair a user has is
disconnect + reconnect, and integration.linear.disconnect deletes every
Linear-synced task (new ids on re-import, workspace links lost).
Why it looks like a desktop cache bug
After a reload the board loads the org's newest 100 tasks (TASK_PAGE_SIZE),
so older stale rows are simply not loaded yet and the board looks right; they
reappear when the next pages load on scroll. task.listPage returns them
exactly as the database holds them.
Suggested fix
A one-off operator re-pull for every Linear connection that was live during the
window: fetch issues with updatedAt inside the window and upsert them through
the same path as initial-sync (guarded by externalUpdatedAt so newer rows
are not overwritten). Longer term, a non-destructive resync (for example a
"Resync" action that runs the initial-sync upsert without the disconnect
delete) would give users a repair path for any lost delivery.
Environment
Desktop 1.30.1 (macOS). Upstream main at 1e89db4b7 for the code
references.
I checked the code at 1e89db4 and the report holds up. There is no backfill or periodic re-sync path, and the fix it suggests would need a small change to the initial-sync upsert first. I could not see the 19 September cutover code, so I can't confirm from the tree why those deliveries were dropped. That part rests on #7675's description.
Summary
On 2026-09-19, Linear issue updates sent to Superset between #7317 reaching production and the #7675 fix were acknowledged but never applied to tasks. The affected rows keep the state they had before that window, so a board shows an old column and completedAt: null days later. The code on main has nothing that would ever repair those rows. Webhooks are the only path that keeps Linear issues in sync after connecting, and the only full re-pull runs once, on connect. The only repair available to users (disconnect, then reconnect) deletes every Linear-synced task. The "desktop cache" symptom is just paging: task.listPage returns what the database holds, and older stale rows only appear once they scroll into a later page.
Affected code
Webhook intake:apps/api/src/app/api/integrations/linear/webhook/route.ts:71-77. hasActiveSubscriber runs before recordWebhookDelivery (:82). When no subscriber is found, the route answers 200 { status: "no_subscribers" } and writes nothing to ingest.webhook_events. Linear doesn't retry a 200, and nothing was stored to replay.
Sweeper:apps/api/src/app/api/integrations/linear/jobs/sweep-abandoned/route.ts:144-158. It only looks at rows already in webhook_events (a rolling 60–75 minute band, pending/failed). Deliveries that were never recorded can't be reached, and the window is long past the band anyway.
Initial sync is the only re-pull:apps/api/src/app/api/integrations/linear/jobs/initial-sync/route.ts. Its only caller is apps/api/src/app/api/integrations/linear/callback/route.ts:89, i.e. OAuth connect. The comments at initial-sync/utils.ts:190 and webhook/processDelivery.ts:325 mention a "periodic sync", but no such job exists. The linear/jobs/* routes are initial-sync, process-delivery, refresh-tokens, sync-task (outbound only) and sweep-abandoned.
Disconnect is destructive:packages/trpc/src/router/integration/linear/linear.ts:48-57 hard-deletes every task with externalProvider = "linear" in the org. After reconnecting, the re-import creates new task ids and loses workspace links, as the report says.
Why nothing heals: no reconciliation exists for Linear issues. The externalUpdatedAt watermark (processDelivery.ts:315-320, :414) only protects against out-of-order webhooks. It can't detect a webhook that never came. A stale row changes only if another webhook arrives for that same issue.
This isn't #5216: mapIssueToTask (initial-sync/utils.ts:187-197) and processIssueEvent (processDelivery.ts:322-330) skip only when the state is missing, and the reporter confirms "Done" is in task_statuses.
Caveat on the suggested fix (reusing the initial-sync upsert)
As written, the initial-sync path can't provide the "guarded by externalUpdatedAt" part:
mapIssueToTask (utils.ts:199-227) never sets externalUpdatedAt.
The upsert at initial-sync/route.ts:157-194 has no setWhere, so it overwrites unconditionally. It also leaves the watermark at its old value, so a stale redelivery could later revert the repaired row.
Before reusing it, three changes are needed:
add updatedAt to the query;
set externalUpdatedAt in mapIssueToTask;
use the same setWhere as processDelivery.ts:414: external_updated_at IS NULL OR external_updated_at < excluded.external_updated_at.
fetchAllIssues (utils.ts:140-164) also hard-codes a 3-month updatedAt >= ... filter. A window re-pull needs that filter as a parameter, with both a lower and an upper bound.
Reproduction
In the data: for an affected org, compare tasks.status_id, completed_at and external_updated_at with Linear's issue.updatedAt and state for the same external_id. Stale rows have external_updated_at < Linear updatedAt, and there's no ingest.webhook_events row for the missing update.
bun:test sketch:
Mock hasActiveSubscriber → false and POST a valid signed Issue.update to the webhook route. Assert a 200 response and that recordWebhookDelivery was never called.
Then flip the mock back to true. Assert that nothing (sweeper included) re-reads that issue.
For the upsert caveat: seed a task with externalUpdatedAt = T2, then run the initial-sync upsert with an issue whose data is from an older time T1. Today the row is overwritten; with the guard it should be left alone.
Suggested fix direction
One-off operator backfill: for each Linear connection live during the window, fetch issues with updatedAt inside it and upsert them through the guarded path above. That's either the patched initial-sync upsert or processIssueEvent's upsert fed from the API response.
Non-destructive "Resync" action: run syncWorkflowStates plus the guarded issue upsert, without the delete in linear.ts:48-57. That gives users a repair path for any future lost delivery.
Optionally: record no_subscribers deliveries in webhook_events with a terminal status instead of dropping them, so a future miss can be replayed.
Optionally: add a scheduled light reconciliation (issues with updatedAt > last run) and fix the misleading "periodic sync" comments.
Activity
github-actions commentedon Sep 22, 2026
I checked the code at
1e89db4and the report holds up. There is no backfill or periodic re-sync path, and the fix it suggests would need a small change to the initial-sync upsert first. I could not see the 19 September cutover code, so I can't confirm from the tree why those deliveries were dropped. That part rests on #7675's description.Summary
On 2026-09-19, Linear issue updates sent to Superset between #7317 reaching production and the #7675 fix were acknowledged but never applied to
tasks. The affected rows keep the state they had before that window, so a board shows an old column andcompletedAt: nulldays later. The code onmainhas nothing that would ever repair those rows. Webhooks are the only path that keeps Linear issues in sync after connecting, and the only full re-pull runs once, on connect. The only repair available to users (disconnect, then reconnect) deletes every Linear-synced task. The "desktop cache" symptom is just paging:task.listPagereturns what the database holds, and older stale rows only appear once they scroll into a later page.Affected code
apps/api/src/app/api/integrations/linear/webhook/route.ts:71-77.hasActiveSubscriberruns beforerecordWebhookDelivery(:82). When no subscriber is found, the route answers200 { status: "no_subscribers" }and writes nothing toingest.webhook_events. Linear doesn't retry a 200, and nothing was stored to replay.apps/api/src/app/api/integrations/linear/jobs/sweep-abandoned/route.ts:144-158. It only looks at rows already inwebhook_events(a rolling 60–75 minute band,pending/failed). Deliveries that were never recorded can't be reached, and the window is long past the band anyway.apps/api/src/app/api/integrations/linear/jobs/initial-sync/route.ts. Its only caller isapps/api/src/app/api/integrations/linear/callback/route.ts:89, i.e. OAuth connect. The comments atinitial-sync/utils.ts:190andwebhook/processDelivery.ts:325mention a "periodic sync", but no such job exists. Thelinear/jobs/*routes areinitial-sync,process-delivery,refresh-tokens,sync-task(outbound only) andsweep-abandoned.packages/trpc/src/router/integration/linear/linear.ts:48-57hard-deletes every task withexternalProvider = "linear"in the org. After reconnecting, the re-import creates new task ids and loses workspace links, as the report says.Root cause
route.ts:71. I can't confirm frommainwhy it returned no subscriber during the window.externalUpdatedAtwatermark (processDelivery.ts:315-320,:414) only protects against out-of-order webhooks. It can't detect a webhook that never came. A stale row changes only if another webhook arrives for that same issue.This isn't #5216:
mapIssueToTask(initial-sync/utils.ts:187-197) andprocessIssueEvent(processDelivery.ts:322-330) skip only when the state is missing, and the reporter confirms "Done" is intask_statuses.Caveat on the suggested fix (reusing the initial-sync upsert)
As written, the initial-sync path can't provide the "guarded by
externalUpdatedAt" part:ISSUES_QUERY(initial-sync/utils.ts:88-138) doesn't selectupdatedAt.mapIssueToTask(utils.ts:199-227) never setsexternalUpdatedAt.initial-sync/route.ts:157-194has nosetWhere, so it overwrites unconditionally. It also leaves the watermark at its old value, so a stale redelivery could later revert the repaired row.Before reusing it, three changes are needed:
updatedAtto the query;externalUpdatedAtinmapIssueToTask;setWhereasprocessDelivery.ts:414:external_updated_at IS NULL OR external_updated_at < excluded.external_updated_at.fetchAllIssues(utils.ts:140-164) also hard-codes a 3-monthupdatedAt >= ...filter. A window re-pull needs that filter as a parameter, with both a lower and an upper bound.Reproduction
tasks.status_id,completed_atandexternal_updated_atwith Linear'sissue.updatedAtandstatefor the sameexternal_id. Stale rows haveexternal_updated_at< LinearupdatedAt, and there's noingest.webhook_eventsrow for the missing update.bun:testsketch:hasActiveSubscriber→falseand POST a valid signedIssue.updateto the webhook route. Assert a 200 response and thatrecordWebhookDeliverywas never called.true. Assert that nothing (sweeper included) re-reads that issue.externalUpdatedAt = T2, then run the initial-sync upsert with an issue whose data is from an older timeT1. Today the row is overwritten; with the guard it should be left alone.Suggested fix direction
updatedAtinside it and upsert them through the guarded path above. That's either the patched initial-sync upsert orprocessIssueEvent's upsert fed from the API response.syncWorkflowStatesplus the guarded issue upsert, without the delete inlinear.ts:48-57. That gives users a repair path for any future lost delivery.no_subscribersdeliveries inwebhook_eventswith a terminal status instead of dropping them, so a future miss can be replayed.updatedAt> last run) and fix the misleading "periodic sync" comments.