patchrelay 0.30.0 → 0.30.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.30.0",
4
- "commit": "88eca79d5f8d",
5
- "builtAt": "2026-04-01T02:27:09.365Z"
3
+ "version": "0.30.1",
4
+ "commit": "dee1d8b522b1",
5
+ "builtAt": "2026-04-01T08:31:08.208Z"
6
6
  }
@@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS issues (
7
7
  title TEXT,
8
8
  url TEXT,
9
9
  current_linear_state TEXT,
10
+ current_linear_state_type TEXT,
10
11
  factory_state TEXT NOT NULL DEFAULT 'delegated',
11
12
  pending_run_type TEXT,
12
13
  pending_run_context_json TEXT,
@@ -152,6 +153,7 @@ CREATE TABLE IF NOT EXISTS issue_dependencies (
152
153
  blocker_issue_key TEXT,
153
154
  blocker_title TEXT,
154
155
  blocker_current_linear_state TEXT,
156
+ blocker_current_linear_state_type TEXT,
155
157
  updated_at TEXT NOT NULL,
156
158
  PRIMARY KEY (project_id, linear_issue_id, blocker_linear_issue_id)
157
159
  );
@@ -188,6 +190,8 @@ export function runPatchRelayMigrations(connection) {
188
190
  addColumnIfMissing(connection, "issues", "description", "TEXT");
189
191
  addColumnIfMissing(connection, "issues", "priority", "INTEGER");
190
192
  addColumnIfMissing(connection, "issues", "estimate", "REAL");
193
+ addColumnIfMissing(connection, "issues", "current_linear_state_type", "TEXT");
194
+ addColumnIfMissing(connection, "issue_dependencies", "blocker_current_linear_state_type", "TEXT");
191
195
  // Zombie/stale recovery backoff
192
196
  addColumnIfMissing(connection, "issues", "zombie_recovery_attempts", "INTEGER NOT NULL DEFAULT 0");
193
197
  addColumnIfMissing(connection, "issues", "last_zombie_recovery_at", "TEXT");
package/dist/db.js CHANGED
@@ -104,6 +104,10 @@ export class PatchRelayDatabase {
104
104
  sets.push("current_linear_state = COALESCE(@currentLinearState, current_linear_state)");
105
105
  values.currentLinearState = params.currentLinearState;
106
106
  }
107
+ if (params.currentLinearStateType !== undefined) {
108
+ sets.push("current_linear_state_type = COALESCE(@currentLinearStateType, current_linear_state_type)");
109
+ values.currentLinearStateType = params.currentLinearStateType;
110
+ }
107
111
  if (params.factoryState !== undefined) {
108
112
  sets.push("factory_state = @factoryState");
109
113
  values.factoryState = params.factoryState;
@@ -207,7 +211,7 @@ export class PatchRelayDatabase {
207
211
  INSERT INTO issues (
208
212
  project_id, linear_issue_id, issue_key, title, description, url,
209
213
  priority, estimate,
210
- current_linear_state, factory_state, pending_run_type, pending_run_context_json,
214
+ current_linear_state, current_linear_state_type, factory_state, pending_run_type, pending_run_context_json,
211
215
  branch_name, worktree_path, thread_id, active_run_id,
212
216
  agent_session_id,
213
217
  pr_number, pr_url, pr_state, pr_review_state, pr_check_status,
@@ -216,7 +220,7 @@ export class PatchRelayDatabase {
216
220
  ) VALUES (
217
221
  @projectId, @linearIssueId, @issueKey, @title, @description, @url,
218
222
  @priority, @estimate,
219
- @currentLinearState, @factoryState, @pendingRunType, @pendingRunContextJson,
223
+ @currentLinearState, @currentLinearStateType, @factoryState, @pendingRunType, @pendingRunContextJson,
220
224
  @branchName, @worktreePath, @threadId, @activeRunId,
221
225
  @agentSessionId,
222
226
  @prNumber, @prUrl, @prState, @prReviewState, @prCheckStatus,
@@ -233,6 +237,7 @@ export class PatchRelayDatabase {
233
237
  priority: params.priority ?? null,
234
238
  estimate: params.estimate ?? null,
235
239
  currentLinearState: params.currentLinearState ?? null,
240
+ currentLinearStateType: params.currentLinearStateType ?? null,
236
241
  factoryState: params.factoryState ?? "delegated",
237
242
  pendingRunType: params.pendingRunType ?? null,
238
243
  pendingRunContextJson: params.pendingRunContextJson ?? null,
@@ -295,11 +300,12 @@ export class PatchRelayDatabase {
295
300
  blocker_issue_key,
296
301
  blocker_title,
297
302
  blocker_current_linear_state,
303
+ blocker_current_linear_state_type,
298
304
  updated_at
299
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
305
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
300
306
  `);
301
307
  for (const blocker of params.blockers) {
302
- insert.run(params.projectId, params.linearIssueId, blocker.blockerLinearIssueId, blocker.blockerIssueKey ?? null, blocker.blockerTitle ?? null, blocker.blockerCurrentLinearState ?? null, now);
308
+ insert.run(params.projectId, params.linearIssueId, blocker.blockerLinearIssueId, blocker.blockerIssueKey ?? null, blocker.blockerTitle ?? null, blocker.blockerCurrentLinearState ?? null, blocker.blockerCurrentLinearStateType ?? null, now);
303
309
  }
304
310
  }
305
311
  listIssueDependencies(projectId, linearIssueId) {
@@ -311,6 +317,7 @@ export class PatchRelayDatabase {
311
317
  COALESCE(blockers.issue_key, d.blocker_issue_key) AS blocker_issue_key,
312
318
  COALESCE(blockers.title, d.blocker_title) AS blocker_title,
313
319
  COALESCE(blockers.current_linear_state, d.blocker_current_linear_state) AS blocker_current_linear_state,
320
+ COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type) AS blocker_current_linear_state_type,
314
321
  d.updated_at
315
322
  FROM issue_dependencies d
316
323
  LEFT JOIN issues blockers
@@ -328,6 +335,9 @@ export class PatchRelayDatabase {
328
335
  ...(row.blocker_current_linear_state !== null && row.blocker_current_linear_state !== undefined
329
336
  ? { blockerCurrentLinearState: String(row.blocker_current_linear_state) }
330
337
  : {}),
338
+ ...(row.blocker_current_linear_state_type !== null && row.blocker_current_linear_state_type !== undefined
339
+ ? { blockerCurrentLinearStateType: String(row.blocker_current_linear_state_type) }
340
+ : {}),
331
341
  updatedAt: String(row.updated_at),
332
342
  }));
333
343
  }
@@ -351,7 +361,10 @@ export class PatchRelayDatabase {
351
361
  ON blockers.project_id = d.project_id
352
362
  AND blockers.linear_issue_id = d.blocker_linear_issue_id
353
363
  WHERE d.project_id = ? AND d.linear_issue_id = ?
354
- AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
364
+ AND (
365
+ COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
366
+ AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
367
+ )
355
368
  `).get(projectId, linearIssueId);
356
369
  return Number(row?.count ?? 0);
357
370
  }
@@ -370,7 +383,10 @@ export class PatchRelayDatabase {
370
383
  AND blockers.linear_issue_id = d.blocker_linear_issue_id
371
384
  WHERE d.project_id = i.project_id
372
385
  AND d.linear_issue_id = i.linear_issue_id
373
- AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
386
+ AND (
387
+ COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
388
+ AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
389
+ )
374
390
  )
375
391
  `)
376
392
  .all();
@@ -491,6 +507,7 @@ export class PatchRelayDatabase {
491
507
  // ─── View builders ──────────────────────────────────────────────
492
508
  issueToTrackedIssue(issue) {
493
509
  const blockedBy = this.listIssueDependencies(issue.projectId, issue.linearIssueId);
510
+ const unresolvedBlockedBy = blockedBy.filter((entry) => !isResolvedLinearState(entry.blockerCurrentLinearStateType, entry.blockerCurrentLinearState));
494
511
  return {
495
512
  id: issue.id,
496
513
  projectId: issue.projectId,
@@ -500,11 +517,10 @@ export class PatchRelayDatabase {
500
517
  ...(issue.url ? { issueUrl: issue.url } : {}),
501
518
  ...(issue.currentLinearState ? { currentLinearState: issue.currentLinearState } : {}),
502
519
  factoryState: issue.factoryState,
503
- blockedByCount: blockedBy.filter((entry) => !isDoneState(entry.blockerCurrentLinearState)).length,
504
- blockedByKeys: blockedBy
505
- .filter((entry) => !isDoneState(entry.blockerCurrentLinearState))
520
+ blockedByCount: unresolvedBlockedBy.length,
521
+ blockedByKeys: unresolvedBlockedBy
506
522
  .map((entry) => entry.blockerIssueKey ?? entry.blockerLinearIssueId),
507
- readyForExecution: issue.pendingRunType !== undefined && issue.activeRunId === undefined,
523
+ readyForExecution: issue.pendingRunType !== undefined && issue.activeRunId === undefined && unresolvedBlockedBy.length === 0,
508
524
  ...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
509
525
  ...(issue.agentSessionId ? { activeAgentSessionId: issue.agentSessionId } : {}),
510
526
  updatedAt: issue.updatedAt,
@@ -544,6 +560,9 @@ function mapIssueRow(row) {
544
560
  ...(row.priority !== null && row.priority !== undefined ? { priority: Number(row.priority) } : {}),
545
561
  ...(row.estimate !== null && row.estimate !== undefined ? { estimate: Number(row.estimate) } : {}),
546
562
  ...(row.current_linear_state !== null ? { currentLinearState: String(row.current_linear_state) } : {}),
563
+ ...(row.current_linear_state_type !== null && row.current_linear_state_type !== undefined
564
+ ? { currentLinearStateType: String(row.current_linear_state_type) }
565
+ : {}),
547
566
  factoryState: String(row.factory_state ?? "delegated"),
548
567
  ...(row.pending_run_type !== null && row.pending_run_type !== undefined ? { pendingRunType: String(row.pending_run_type) } : {}),
549
568
  ...(row.pending_run_context_json !== null && row.pending_run_context_json !== undefined ? { pendingRunContextJson: String(row.pending_run_context_json) } : {}),
@@ -602,6 +621,6 @@ function mapRunRow(row) {
602
621
  ...(row.ended_at !== null ? { endedAt: String(row.ended_at) } : {}),
603
622
  };
604
623
  }
605
- function isDoneState(stateName) {
606
- return stateName?.trim().toLowerCase() === "done";
624
+ function isResolvedLinearState(stateType, stateName) {
625
+ return stateType === "completed" || stateName?.trim().toLowerCase() === "done";
607
626
  }
@@ -15,6 +15,7 @@ const LINEAR_ISSUE_SELECTION = `
15
15
  state {
16
16
  id
17
17
  name
18
+ type
18
19
  }
19
20
  labels {
20
21
  nodes {
@@ -317,6 +318,7 @@ export class LinearGraphqlClient {
317
318
  ...(issue.estimate != null ? { estimate: issue.estimate } : {}),
318
319
  ...(issue.state?.id ? { stateId: issue.state.id } : {}),
319
320
  ...(issue.state?.name ? { stateName: issue.state.name } : {}),
321
+ ...(issue.state?.type ? { stateType: issue.state.type } : {}),
320
322
  ...(issue.team?.id ? { teamId: issue.team.id } : {}),
321
323
  ...(issue.team?.key ? { teamKey: issue.team.key } : {}),
322
324
  ...(issue.delegate?.id ? { delegateId: issue.delegate.id } : {}),
package/dist/service.js CHANGED
@@ -229,7 +229,10 @@ export class PatchRelayService {
229
229
  AND blockers.linear_issue_id = d.blocker_linear_issue_id
230
230
  WHERE d.project_id = i.project_id
231
231
  AND d.linear_issue_id = i.linear_issue_id
232
- AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
232
+ AND (
233
+ COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
234
+ AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
235
+ )
233
236
  ) AS blocked_by_count,
234
237
  (
235
238
  SELECT json_group_array(COALESCE(blockers.issue_key, d.blocker_issue_key, d.blocker_linear_issue_id))
@@ -239,7 +242,10 @@ export class PatchRelayService {
239
242
  AND blockers.linear_issue_id = d.blocker_linear_issue_id
240
243
  WHERE d.project_id = i.project_id
241
244
  AND d.linear_issue_id = i.linear_issue_id
242
- AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
245
+ AND (
246
+ COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
247
+ AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
248
+ )
243
249
  ) AS blocked_by_keys_json
244
250
  FROM issues i
245
251
  LEFT JOIN runs active_run ON active_run.id = i.active_run_id
@@ -180,6 +180,7 @@ export class WebhookHandler {
180
180
  ...(hydratedIssue.priority != null ? { priority: hydratedIssue.priority } : {}),
181
181
  ...(hydratedIssue.estimate != null ? { estimate: hydratedIssue.estimate } : {}),
182
182
  ...(hydratedIssue.stateName ? { currentLinearState: hydratedIssue.stateName } : {}),
183
+ ...(hydratedIssue.stateType ? { currentLinearStateType: hydratedIssue.stateType } : {}),
183
184
  ...(pendingRunType ? { pendingRunType, factoryState: "delegated" } : {}),
184
185
  ...(clearPendingImplementation ? { pendingRunType: null } : {}),
185
186
  ...((pendingRunType || existingIssue?.pendingRunType === "implementation") && pendingRunContextJson
@@ -203,27 +204,30 @@ export class WebhookHandler {
203
204
  }
204
205
  async syncIssueDependencies(projectId, issue) {
205
206
  let source = issue;
206
- if (source.blockedBy.length === 0 && source.blocks.length === 0) {
207
+ if (!source.relationsKnown) {
207
208
  const linear = await this.linearProvider.forProject(projectId);
208
209
  if (linear) {
209
210
  try {
210
211
  source = mergeIssueMetadata(source, await linear.getIssue(issue.id));
211
212
  }
212
213
  catch {
213
- // Fall back to webhook payload data when live hydration is unavailable.
214
+ // Preserve existing dependency rows when webhook relation data is incomplete.
214
215
  }
215
216
  }
216
217
  }
217
- this.db.replaceIssueDependencies({
218
- projectId,
219
- linearIssueId: source.id,
220
- blockers: source.blockedBy.map((blocker) => ({
221
- blockerLinearIssueId: blocker.id,
222
- ...(blocker.identifier ? { blockerIssueKey: blocker.identifier } : {}),
223
- ...(blocker.title ? { blockerTitle: blocker.title } : {}),
224
- ...(blocker.stateName ? { blockerCurrentLinearState: blocker.stateName } : {}),
225
- })),
226
- });
218
+ if (source.relationsKnown) {
219
+ this.db.replaceIssueDependencies({
220
+ projectId,
221
+ linearIssueId: source.id,
222
+ blockers: source.blockedBy.map((blocker) => ({
223
+ blockerLinearIssueId: blocker.id,
224
+ ...(blocker.identifier ? { blockerIssueKey: blocker.identifier } : {}),
225
+ ...(blocker.title ? { blockerTitle: blocker.title } : {}),
226
+ ...(blocker.stateName ? { blockerCurrentLinearState: blocker.stateName } : {}),
227
+ ...(blocker.stateType ? { blockerCurrentLinearStateType: blocker.stateType } : {}),
228
+ })),
229
+ });
230
+ }
227
231
  return source;
228
232
  }
229
233
  reconcileDependentReadiness(projectId, blockerLinearIssueId) {
@@ -559,10 +563,12 @@ function mergeIssueMetadata(issue, liveIssue) {
559
563
  ...(issue.teamKey ? {} : liveIssue.teamKey ? { teamKey: liveIssue.teamKey } : {}),
560
564
  ...(issue.stateId ? {} : liveIssue.stateId ? { stateId: liveIssue.stateId } : {}),
561
565
  ...(issue.stateName ? {} : liveIssue.stateName ? { stateName: liveIssue.stateName } : {}),
566
+ ...(issue.stateType ? {} : liveIssue.stateType ? { stateType: liveIssue.stateType } : {}),
562
567
  ...(issue.delegateId ? {} : liveIssue.delegateId ? { delegateId: liveIssue.delegateId } : {}),
563
568
  ...(issue.delegateName ? {} : liveIssue.delegateName ? { delegateName: liveIssue.delegateName } : {}),
569
+ relationsKnown: issue.relationsKnown || liveIssue.blockedBy !== undefined || liveIssue.blocks !== undefined,
564
570
  labelNames: issue.labelNames.length > 0 ? issue.labelNames : (liveIssue.labels ?? []).map((l) => l.name),
565
- blockedBy: issue.blockedBy.length > 0 ? issue.blockedBy : (liveIssue.blockedBy ?? []),
566
- blocks: issue.blocks.length > 0 ? issue.blocks : (liveIssue.blocks ?? []),
571
+ blockedBy: issue.relationsKnown ? issue.blockedBy : (liveIssue.blockedBy ?? issue.blockedBy),
572
+ blocks: issue.relationsKnown ? issue.blocks : (liveIssue.blocks ?? issue.blocks),
567
573
  };
568
574
  }
package/dist/webhooks.js CHANGED
@@ -227,6 +227,7 @@ function extractIssueMetadata(payload) {
227
227
  ...(delegateName ? { delegateName } : {}),
228
228
  ...(priority != null ? { priority } : {}),
229
229
  ...(estimate != null ? { estimate } : {}),
230
+ relationsKnown: false,
230
231
  labelNames: extractLabelNames(issueRecord),
231
232
  blockedBy: [],
232
233
  blocks: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {