patchrelay 0.83.0 → 0.83.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.83.0",
4
- "commit": "5e17a07e67bc",
5
- "builtAt": "2026-06-14T17:38:15.238Z"
3
+ "version": "0.83.1",
4
+ "commit": "87265ce29424",
5
+ "builtAt": "2026-06-14T18:10:49.183Z"
6
6
  }
package/dist/db.js CHANGED
@@ -87,6 +87,21 @@ export class PatchRelayDatabase {
87
87
  assertSchemaReady() {
88
88
  assertPatchRelaySchemaReady(this.connection, this.databasePath);
89
89
  }
90
+ describeSchema() {
91
+ const tableRows = this.connection.prepare(`
92
+ SELECT name FROM sqlite_master
93
+ WHERE type = 'table' AND name IN ('issues', 'issue_sessions', 'runs')
94
+ ORDER BY name
95
+ `).all();
96
+ const issueColumns = tableRows.some((row) => row.name === "issues")
97
+ ? this.connection.prepare("PRAGMA table_info(issues)").all().map((row) => row.name)
98
+ : [];
99
+ return {
100
+ databasePath: this.databasePath,
101
+ tables: tableRows.map((row) => row.name),
102
+ issuesVersionColumnPresent: issueColumns.includes("version"),
103
+ };
104
+ }
90
105
  transaction(fn) {
91
106
  return this.connection.transaction(fn)();
92
107
  }
@@ -1,4 +1,5 @@
1
1
  import { deriveLinearProgressFact } from "./linear-progress-facts.js";
2
+ import { isSqliteSchemaReadError } from "./sqlite-errors.js";
2
3
  export class LinearProgressReporter {
3
4
  db;
4
5
  emitActivity;
@@ -11,7 +12,7 @@ export class LinearProgressReporter {
11
12
  this.options = options;
12
13
  }
13
14
  maybeEmitProgress(notification, run) {
14
- const issue = this.db.getIssue(run.projectId, run.linearIssueId);
15
+ const issue = this.getIssueWithSchemaRetry(run);
15
16
  if (!issue) {
16
17
  return;
17
18
  }
@@ -73,7 +74,7 @@ export class LinearProgressReporter {
73
74
  if (previous?.lastHeartbeatAtMs !== undefined && now - previous.lastHeartbeatAtMs < intervalMs) {
74
75
  return;
75
76
  }
76
- const issue = this.db.getIssue(run.projectId, run.linearIssueId);
77
+ const issue = this.getIssueWithSchemaRetry(run);
77
78
  if (!issue) {
78
79
  return;
79
80
  }
@@ -108,6 +109,18 @@ export class LinearProgressReporter {
108
109
  now() {
109
110
  return this.options.now?.() ?? Date.now();
110
111
  }
112
+ getIssueWithSchemaRetry(run) {
113
+ try {
114
+ return this.db.getIssue(run.projectId, run.linearIssueId);
115
+ }
116
+ catch (error) {
117
+ if (!isSqliteSchemaReadError(error)) {
118
+ throw error;
119
+ }
120
+ this.db.assertSchemaReady();
121
+ return this.db.getIssue(run.projectId, run.linearIssueId);
122
+ }
123
+ }
111
124
  clearFailedPublication(runId, channel, meaningKey, publishedAtMs) {
112
125
  const current = this.publicationsByRun.get(runId);
113
126
  if (!current) {
@@ -1,4 +1,5 @@
1
1
  import { classifyIssue } from "./issue-class.js";
2
+ import { reconcileWorkflowTasksForIssue } from "./workflow-task-reconciler.js";
2
3
  const WRITER = "orchestration-parent-wake";
3
4
  export const ORCHESTRATION_SETTLE_WINDOW_MS = 10_000;
4
5
  export function computeOrchestrationSettleUntil(now = Date.now()) {
@@ -23,6 +24,12 @@ function resolveParentIssueIds(db, child) {
23
24
  }
24
25
  return unique(parentIds);
25
26
  }
27
+ function parentHasRunnableWorkflowTask(db, parent) {
28
+ const reconciliation = reconcileWorkflowTasksForIssue(db, parent);
29
+ return reconciliation.result.open.some((task) => (task.taskType === "run"
30
+ && task.runType !== undefined
31
+ && task.gateAction === "start"));
32
+ }
26
33
  export function startOrchestrationSettleWindow(db, issue, now = Date.now()) {
27
34
  const settleUntil = computeOrchestrationSettleUntil(now);
28
35
  db.issueSessions.commitIssueState({
@@ -70,6 +77,10 @@ export function wakeOrchestrationParentsForChildEvent(params) {
70
77
  parentIds.push(parent.linearIssueId);
71
78
  continue;
72
79
  }
80
+ if (!parentHasRunnableWorkflowTask(params.db, parent)) {
81
+ parentIds.push(parent.linearIssueId);
82
+ continue;
83
+ }
73
84
  params.wakeDispatcher.recordEventAndDispatch(parent.projectId, parent.linearIssueId, {
74
85
  eventType: params.eventType,
75
86
  eventJson: JSON.stringify({
@@ -234,7 +234,14 @@ export class RunNotificationHandler {
234
234
  this.linearSync.maybeEmitProgress(notification, run);
235
235
  }
236
236
  catch (error) {
237
- this.logger.warn({ runId: run.id, projectId: run.projectId, issueId: run.linearIssueId, method: notification.method, error: formatError(error) }, "Linear progress reporting failed");
237
+ this.logger.warn({
238
+ runId: run.id,
239
+ projectId: run.projectId,
240
+ issueId: run.linearIssueId,
241
+ method: notification.method,
242
+ error: formatError(error),
243
+ storage: this.safeStorageDiagnostics(),
244
+ }, "Linear progress reporting failed");
238
245
  }
239
246
  }
240
247
  syncCodexPlan(notification, run) {
@@ -257,6 +264,14 @@ export class RunNotificationHandler {
257
264
  this.logger.warn({ runId: run.id, issueKey: issue.issueKey, projectId: run.projectId, issueId: run.linearIssueId, method: notification.method, error: formatError(error) }, "Linear plan sync failed");
258
265
  }
259
266
  }
267
+ safeStorageDiagnostics() {
268
+ try {
269
+ return this.db.describeSchema();
270
+ }
271
+ catch {
272
+ return undefined;
273
+ }
274
+ }
260
275
  }
261
276
  function formatError(error) {
262
277
  return error instanceof Error ? error.message : String(error);
@@ -2,6 +2,7 @@ import { getCiRepairBudget, getQueueRepairBudget, getReviewFixBudget, } from "./
2
2
  import { buildRequestedChangesWakeIdentity } from "./reactive-wake-keys.js";
3
3
  import { parseRunContextOrWarn, serializeRunContext, tryParseRunContextValue } from "./run-context.js";
4
4
  import { assertNever } from "./utils.js";
5
+ import { reconcileWorkflowTasksForIssue } from "./workflow-task-reconciler.js";
5
6
  const WRITER = "run-wake-planner";
6
7
  function parseObjectJson(raw) {
7
8
  if (!raw)
@@ -24,11 +25,22 @@ export class RunWakePlanner {
24
25
  this.logger = logger;
25
26
  }
26
27
  resolveRunWake(issue) {
27
- if (this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId) > 0) {
28
+ const freshIssue = this.db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
29
+ if (this.db.issues.countUnresolvedBlockers(freshIssue.projectId, freshIssue.linearIssueId) > 0) {
28
30
  return undefined;
29
31
  }
30
- const sessionWake = this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId);
32
+ const existingWorkflowTaskWake = this.resolveWorkflowTaskWake(freshIssue);
33
+ if (existingWorkflowTaskWake)
34
+ return existingWorkflowTaskWake;
35
+ this.reconcileWorkflowTasks(freshIssue);
36
+ const workflowTaskWake = this.resolveWorkflowTaskWake(freshIssue);
37
+ if (workflowTaskWake)
38
+ return workflowTaskWake;
39
+ const sessionWake = this.db.issueSessions.peekIssueSessionWake(freshIssue.projectId, freshIssue.linearIssueId);
31
40
  if (sessionWake) {
41
+ if (this.workflowTasksSuppressSessionWake(freshIssue, sessionWake.wakeReason)) {
42
+ return undefined;
43
+ }
32
44
  return {
33
45
  runType: sessionWake.runType,
34
46
  context: sessionWake.context,
@@ -37,10 +49,10 @@ export class RunWakePlanner {
37
49
  eventIds: sessionWake.eventIds,
38
50
  };
39
51
  }
40
- const workflowTaskWake = this.resolveWorkflowTaskWake(issue);
41
- if (workflowTaskWake)
42
- return workflowTaskWake;
43
- const implicitWake = this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId);
52
+ if (this.workflowTasksSuppressSessionWake(freshIssue, undefined)) {
53
+ return undefined;
54
+ }
55
+ const implicitWake = this.db.workflowWakes.peekIssueWake(freshIssue.projectId, freshIssue.linearIssueId);
44
56
  if (!implicitWake)
45
57
  return undefined;
46
58
  return {
@@ -72,6 +84,56 @@ export class RunWakePlanner {
72
84
  eventIds: [],
73
85
  };
74
86
  }
87
+ reconcileWorkflowTasks(issue) {
88
+ try {
89
+ reconcileWorkflowTasksForIssue(this.db, issue);
90
+ }
91
+ catch (error) {
92
+ this.logger?.warn({
93
+ projectId: issue.projectId,
94
+ linearIssueId: issue.linearIssueId,
95
+ ...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
96
+ error: error instanceof Error ? error.message : String(error),
97
+ }, "Workflow task reconciliation failed while planning run wake");
98
+ }
99
+ }
100
+ workflowTasksSuppressSessionWake(issue, wakeReason) {
101
+ const openTasks = this.db.workflowTasks.listOpenTasks(issue.projectId, issue.linearIssueId);
102
+ if (openTasks.length === 0)
103
+ return false;
104
+ if (openTasks.some((task) => task.taskType === "run" && task.gateAction === "start" && task.runType !== undefined)) {
105
+ return false;
106
+ }
107
+ if (!openTasks.some((task) => this.isBlockingWorkflowGate(task)))
108
+ return false;
109
+ if (!openTasks.every((task) => task.taskId === "wait:input")) {
110
+ return true;
111
+ }
112
+ return wakeReason !== "direct_reply"
113
+ && wakeReason !== "followup_prompt"
114
+ && wakeReason !== "followup_comment"
115
+ && wakeReason !== "human_instruction"
116
+ && wakeReason !== "operator_prompt"
117
+ && wakeReason !== "completion_check_continue";
118
+ }
119
+ isBlockingWorkflowGate(task) {
120
+ if (task.taskId === "wait:input")
121
+ return true;
122
+ if (task.taskId === "wait:children" || task.taskId === "wait:blockers" || task.taskId.startsWith("wait:active-run:")) {
123
+ return true;
124
+ }
125
+ if (task.taskId === "wait:authority") {
126
+ return this.workflowAuthorityObserved(task.projectId, task.subjectId);
127
+ }
128
+ return task.taskType === "verify" || task.taskType === "ask" || task.taskType === "escalate" || task.taskType === "publish";
129
+ }
130
+ workflowAuthorityObserved(projectId, linearIssueId) {
131
+ return this.db.workflowObservations
132
+ .listObservations(projectId, linearIssueId)
133
+ .some((observation) => (observation.type === "linear.delegated"
134
+ || observation.type === "linear.undelegated"
135
+ || observation.type === "operator.authority_changed"));
136
+ }
75
137
  appendWakeEventWithLease(lease, issue, runType, context, dedupeScope) {
76
138
  let eventType;
77
139
  let dedupeKey;
@@ -1,5 +1,6 @@
1
1
  import { SerialWorkQueue } from "./service-queue.js";
2
2
  import { retrySqliteLockedQueueFailure } from "./queue-failure-policy.js";
3
+ import { isSqliteSchemaReadError } from "./sqlite-errors.js";
3
4
  const ISSUE_KEY_DELIMITER = "::";
4
5
  const DEFAULT_RECONCILE_INTERVAL_MS = 5_000;
5
6
  const DEFAULT_RECONCILE_TIMEOUT_MS = 60_000;
@@ -135,7 +136,7 @@ export class ServiceRuntime {
135
136
  }
136
137
  this.reconcileInProgress = true;
137
138
  try {
138
- await promiseWithTimeout(this.runReconciler.reconcileActiveRuns(), this.options.reconcileTimeoutMs ?? DEFAULT_RECONCILE_TIMEOUT_MS, "Background active-run reconciliation");
139
+ await this.reconcileActiveRunsWithSchemaRetry();
139
140
  // Pick up issues that became ready outside the webhook path
140
141
  // (e.g. CLI retry, manual DB edits) without requiring a restart.
141
142
  for (const issue of this.readyIssueSource.listIssuesReadyForExecution()) {
@@ -143,7 +144,10 @@ export class ServiceRuntime {
143
144
  }
144
145
  }
145
146
  catch (error) {
146
- this.logger.warn({ error: error instanceof Error ? error.message : String(error) }, "Background active-run reconciliation failed");
147
+ this.logger.warn({
148
+ error: error instanceof Error ? error.message : String(error),
149
+ storage: this.safeStorageDiagnostics(),
150
+ }, "Background active-run reconciliation failed");
147
151
  }
148
152
  finally {
149
153
  this.reconcileInProgress = false;
@@ -152,6 +156,30 @@ export class ServiceRuntime {
152
156
  }
153
157
  }
154
158
  }
159
+ async reconcileActiveRunsWithSchemaRetry() {
160
+ try {
161
+ await this.reconcileActiveRunsOnce();
162
+ }
163
+ catch (error) {
164
+ if (!isSqliteSchemaReadError(error) || !this.options.assertStorageReady) {
165
+ throw error;
166
+ }
167
+ this.options.assertStorageReady();
168
+ await new Promise((resolve) => setTimeout(resolve, 100));
169
+ await this.reconcileActiveRunsOnce();
170
+ }
171
+ }
172
+ async reconcileActiveRunsOnce() {
173
+ await promiseWithTimeout(this.runReconciler.reconcileActiveRuns(), this.options.reconcileTimeoutMs ?? DEFAULT_RECONCILE_TIMEOUT_MS, "Background active-run reconciliation");
174
+ }
175
+ safeStorageDiagnostics() {
176
+ try {
177
+ return this.options.describeStorage?.();
178
+ }
179
+ catch {
180
+ return undefined;
181
+ }
182
+ }
155
183
  getMaxActiveIssueRuns() {
156
184
  const configured = this.options.maxActiveIssueRuns ?? DEFAULT_MAX_ACTIVE_ISSUE_RUNS;
157
185
  return Math.max(1, Math.floor(configured));
package/dist/service.js CHANGED
@@ -77,6 +77,9 @@ export class PatchRelayService {
77
77
  processIssue: async (item) => {
78
78
  await this.orchestrator.run(item);
79
79
  },
80
+ }, {
81
+ assertStorageReady: () => db.assertSchemaReady(),
82
+ describeStorage: () => db.describeSchema(),
80
83
  });
81
84
  enqueueIssue = (projectId, issueId) => runtime.enqueueIssue(projectId, issueId);
82
85
  this.oauthService = new LinearOAuthService(config, { linearInstallations: db.linearInstallations }, logger);
@@ -0,0 +1,5 @@
1
+ export function isSqliteSchemaReadError(error) {
2
+ const message = error instanceof Error ? error.message : String(error);
3
+ return message.includes("no such table:")
4
+ || message.includes("no such column:");
5
+ }
@@ -1,4 +1,5 @@
1
1
  import { emitTelemetry, noopTelemetry } from "./telemetry.js";
2
+ import { reconcileWorkflowTasksForIssue } from "./workflow-task-reconciler.js";
2
3
  // Single owner of "append a session event and tell the orchestrator
3
4
  // something might be runnable", and of "release a finished run so the
4
5
  // next wake fires." Until this existed, 8+ call sites each made their
@@ -32,22 +33,82 @@ export class WakeDispatcher {
32
33
  this.feed = feed;
33
34
  this.telemetry = telemetry;
34
35
  }
35
- peekRunnableWorkflowTask(projectId, linearIssueId) {
36
- return this.db.workflowTasks
37
- .listOpenRunnableTasks(projectId)
38
- .find((task) => task.subjectId === linearIssueId && task.runType !== undefined);
36
+ listOpenWorkflowTasks(projectId, linearIssueId) {
37
+ return this.db.workflowTasks.listOpenTasks(projectId, linearIssueId);
39
38
  }
40
- resolveDispatchableWake(projectId, linearIssueId, issue) {
41
- const sessionWake = this.db.issueSessions.peekIssueSessionWake(projectId, linearIssueId);
42
- if (sessionWake) {
39
+ reconcileOpenWorkflowTasks(issue, options) {
40
+ try {
41
+ return reconcileWorkflowTasksForIssue(this.db, issue, options).result.open;
42
+ }
43
+ catch (error) {
44
+ this.logger.warn({
45
+ projectId: issue.projectId,
46
+ linearIssueId: issue.linearIssueId,
47
+ ...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
48
+ error: error instanceof Error ? error.message : String(error),
49
+ }, "Workflow task reconciliation failed while resolving wake");
50
+ return this.listOpenWorkflowTasks(issue.projectId, issue.linearIssueId);
51
+ }
52
+ }
53
+ peekRunnableWorkflowTask(projectId, linearIssueId, openTasks) {
54
+ return (openTasks ?? this.db.workflowTasks.listOpenRunnableTasks(projectId))
55
+ .find((task) => (task.subjectId === linearIssueId
56
+ && task.taskType === "run"
57
+ && task.gateAction === "start"
58
+ && task.runType !== undefined));
59
+ }
60
+ workflowAuthorityObserved(projectId, linearIssueId) {
61
+ return this.db.workflowObservations
62
+ .listObservations(projectId, linearIssueId)
63
+ .some((observation) => (observation.type === "linear.delegated"
64
+ || observation.type === "linear.undelegated"
65
+ || observation.type === "operator.authority_changed"));
66
+ }
67
+ sessionWakeCanAnswerInputWait(openTasks, wakeReason) {
68
+ if (openTasks.length === 0 || !openTasks.every((task) => task.taskId === "wait:input")) {
69
+ return false;
70
+ }
71
+ return wakeReason === "direct_reply"
72
+ || wakeReason === "followup_prompt"
73
+ || wakeReason === "followup_comment"
74
+ || wakeReason === "human_instruction"
75
+ || wakeReason === "operator_prompt"
76
+ || wakeReason === "completion_check_continue";
77
+ }
78
+ workflowTasksSuppressSessionWake(openTasks, wakeReason) {
79
+ if (openTasks.length === 0)
80
+ return false;
81
+ if (this.peekRunnableWorkflowTask(openTasks[0].projectId, openTasks[0].subjectId, openTasks))
82
+ return false;
83
+ if (!openTasks.some((task) => this.isBlockingWorkflowGate(task)))
84
+ return false;
85
+ return !this.sessionWakeCanAnswerInputWait(openTasks, wakeReason);
86
+ }
87
+ isBlockingWorkflowGate(task) {
88
+ if (task.taskId === "wait:input")
89
+ return true;
90
+ if (task.taskId === "wait:children" || task.taskId === "wait:blockers" || task.taskId.startsWith("wait:active-run:")) {
91
+ return true;
92
+ }
93
+ if (task.taskId === "wait:authority") {
94
+ return this.workflowAuthorityObserved(task.projectId, task.subjectId);
95
+ }
96
+ return task.taskType === "verify" || task.taskType === "ask" || task.taskType === "escalate" || task.taskType === "publish";
97
+ }
98
+ resolveDispatchableWake(projectId, linearIssueId, issue, options) {
99
+ const existingWorkflowTasks = this.listOpenWorkflowTasks(projectId, linearIssueId);
100
+ const existingWorkflowTask = this.peekRunnableWorkflowTask(projectId, linearIssueId, existingWorkflowTasks);
101
+ if (existingWorkflowTask?.runType) {
43
102
  return {
44
- runType: sessionWake.runType,
45
- ...(sessionWake.wakeReason ? { wakeReason: sessionWake.wakeReason } : {}),
46
- eventIds: sessionWake.eventIds,
47
- source: "session_event",
103
+ runType: existingWorkflowTask.runType,
104
+ wakeReason: existingWorkflowTask.taskId,
105
+ eventIds: [],
106
+ source: "workflow_task",
48
107
  };
49
108
  }
50
- const workflowTask = this.peekRunnableWorkflowTask(projectId, linearIssueId);
109
+ const freshIssue = this.db.issues.getIssue(projectId, linearIssueId) ?? issue;
110
+ const openWorkflowTasks = this.reconcileOpenWorkflowTasks(freshIssue, options);
111
+ const workflowTask = this.peekRunnableWorkflowTask(projectId, linearIssueId, openWorkflowTasks);
51
112
  if (workflowTask?.runType) {
52
113
  return {
53
114
  runType: workflowTask.runType,
@@ -56,6 +117,21 @@ export class WakeDispatcher {
56
117
  source: "workflow_task",
57
118
  };
58
119
  }
120
+ const sessionWake = this.db.issueSessions.peekIssueSessionWake(projectId, linearIssueId);
121
+ if (sessionWake) {
122
+ if (this.workflowTasksSuppressSessionWake(openWorkflowTasks, sessionWake.wakeReason)) {
123
+ return undefined;
124
+ }
125
+ return {
126
+ runType: sessionWake.runType,
127
+ ...(sessionWake.wakeReason ? { wakeReason: sessionWake.wakeReason } : {}),
128
+ eventIds: sessionWake.eventIds,
129
+ source: "session_event",
130
+ };
131
+ }
132
+ if (this.workflowTasksSuppressSessionWake(openWorkflowTasks, undefined)) {
133
+ return undefined;
134
+ }
59
135
  if (issue.pendingRunType) {
60
136
  return {
61
137
  runType: issue.pendingRunType,
@@ -279,7 +355,17 @@ export class WakeDispatcher {
279
355
  runType: params.run.runType,
280
356
  });
281
357
  const issue = this.db.issues.getIssue(params.run.projectId, params.run.linearIssueId);
282
- const wake = issue ? this.resolveDispatchableWake(params.run.projectId, params.run.linearIssueId, issue) : undefined;
358
+ if (issue?.factoryState === "done" || issue?.factoryState === "failed" || issue?.factoryState === "escalated" || issue?.prState === "merged") {
359
+ emitTelemetry(this.telemetry, {
360
+ type: "wake.suppressed",
361
+ projectId: params.run.projectId,
362
+ linearIssueId: params.run.linearIssueId,
363
+ ...(params.issueKey ? { issueKey: params.issueKey } : {}),
364
+ reason: "terminal_event",
365
+ });
366
+ return undefined;
367
+ }
368
+ const wake = issue ? this.resolveDispatchableWake(params.run.projectId, params.run.linearIssueId, issue, { ignoreDetachedActiveRuns: true }) : undefined;
283
369
  if (!wake) {
284
370
  emitTelemetry(this.telemetry, {
285
371
  type: "wake.suppressed",
@@ -197,6 +197,9 @@ export function deriveWorkflowTasks(snapshot) {
197
197
  if (snapshot.status === "done") {
198
198
  return [];
199
199
  }
200
+ if (snapshot.status === "failed") {
201
+ return [];
202
+ }
200
203
  if (snapshot.activeRun) {
201
204
  return [{
202
205
  id: `wait:active-run:${snapshot.activeRun.id}`,
@@ -2,13 +2,21 @@ import { evaluateTaskStart, projectWorkflowSnapshot, } from "./workflow-runtime.
2
2
  function isActiveRun(run) {
3
3
  return run.status === "queued" || run.status === "running";
4
4
  }
5
- function resolveActiveRunSnapshot(db, issue) {
5
+ function resolveActiveRunSnapshot(db, issue, options) {
6
6
  const pinnedRun = issue.activeRunId !== undefined ? db.runs.getRunById(issue.activeRunId) : undefined;
7
- const run = pinnedRun && isActiveRun(pinnedRun)
8
- ? pinnedRun
9
- : db.runs.listRunsForIssue(issue.projectId, issue.linearIssueId)
10
- .filter(isActiveRun)
11
- .at(-1);
7
+ if (pinnedRun && isActiveRun(pinnedRun)) {
8
+ return {
9
+ id: pinnedRun.id,
10
+ runType: pinnedRun.runType,
11
+ authorityEpoch: pinnedRun.authorityEpoch,
12
+ status: pinnedRun.status,
13
+ };
14
+ }
15
+ if (options?.ignoreDetachedActiveRuns)
16
+ return undefined;
17
+ const run = db.runs.listRunsForIssue(issue.projectId, issue.linearIssueId)
18
+ .filter(isActiveRun)
19
+ .at(-1);
12
20
  if (!run)
13
21
  return undefined;
14
22
  return {
@@ -34,8 +42,8 @@ function readinessForTask(snapshot, task) {
34
42
  }
35
43
  return evaluateTaskStart(snapshot, task);
36
44
  }
37
- export function buildWorkflowSnapshotForIssue(db, issue) {
38
- const activeRun = resolveActiveRunSnapshot(db, issue);
45
+ export function buildWorkflowSnapshotForIssue(db, issue, options) {
46
+ const activeRun = resolveActiveRunSnapshot(db, issue, options);
39
47
  return projectWorkflowSnapshot({
40
48
  issue,
41
49
  observations: db.workflowObservations.listObservations(issue.projectId, issue.linearIssueId),
@@ -45,8 +53,8 @@ export function buildWorkflowSnapshotForIssue(db, issue) {
45
53
  ...(activeRun ? { activeRun } : {}),
46
54
  });
47
55
  }
48
- export function reconcileWorkflowTasksForIssue(db, issue) {
49
- const snapshot = buildWorkflowSnapshotForIssue(db, issue);
56
+ export function reconcileWorkflowTasksForIssue(db, issue, options) {
57
+ const snapshot = buildWorkflowSnapshotForIssue(db, issue, options);
50
58
  const result = db.workflowTasks.reconcileTasks({
51
59
  projectId: issue.projectId,
52
60
  subjectId: issue.linearIssueId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.83.0",
3
+ "version": "0.83.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {