patchrelay 0.49.0 → 0.49.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.49.0",
4
- "commit": "e4ca4c92eb96",
5
- "builtAt": "2026-04-19T10:24:09.505Z"
3
+ "version": "0.49.1",
4
+ "commit": "b14320b27556",
5
+ "builtAt": "2026-04-19T11:22:46.139Z"
6
6
  }
package/dist/cli/data.js CHANGED
@@ -9,6 +9,8 @@ import { buildManualRetryAttemptReset, resolveRetryTarget } from "../manual-issu
9
9
  import { WorktreeManager } from "../worktree-manager.js";
10
10
  import { parseDelegationObservedPayload, parseRunReleasedAuthorityPayload } from "../delegation-audit.js";
11
11
  import { CliOperatorApiClient } from "./operator-client.js";
12
+ import { resolveEffectiveActiveRun } from "../effective-active-run.js";
13
+ import { derivePatchRelayWaitingReason } from "../waiting-reason.js";
12
14
  function safeJsonParse(value) {
13
15
  if (!value)
14
16
  return undefined;
@@ -105,8 +107,11 @@ export class CliDataAccess extends CliOperatorApiClient {
105
107
  if (!issue)
106
108
  return undefined;
107
109
  const dbIssue = this.db.issues.getIssueByKey(issueKey);
108
- const activeRun = dbIssue.activeRunId ? this.db.runs.getRunById(dbIssue.activeRunId) : undefined;
109
110
  const latestRun = this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
111
+ const activeRun = resolveEffectiveActiveRun({
112
+ activeRun: dbIssue.activeRunId ? this.db.runs.getRunById(dbIssue.activeRunId) : undefined,
113
+ latestRun,
114
+ });
110
115
  const latestReport = normalizeStageReport(latestRun?.reportJson, latestRun?.status);
111
116
  const latestSummary = safeJsonParse(latestRun?.summaryJson);
112
117
  const completionCheck = latestRun ? extractCompletionCheck(latestRun) : undefined;
@@ -139,7 +144,10 @@ export class CliDataAccess extends CliOperatorApiClient {
139
144
  if (!issue)
140
145
  return undefined;
141
146
  const dbIssue = this.db.issues.getIssueByKey(issueKey);
142
- const run = dbIssue.activeRunId ? this.db.runs.getRunById(dbIssue.activeRunId) : undefined;
147
+ const run = resolveEffectiveActiveRun({
148
+ activeRun: dbIssue.activeRunId ? this.db.runs.getRunById(dbIssue.activeRunId) : undefined,
149
+ latestRun: this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId),
150
+ });
143
151
  if (!run)
144
152
  return undefined;
145
153
  const live = run.threadId &&
@@ -469,19 +477,36 @@ export class CliDataAccess extends CliOperatorApiClient {
469
477
  ORDER BY i.updated_at DESC, i.issue_key ASC
470
478
  `)
471
479
  .all(...values);
472
- const items = rows.map((row) => ({
473
- ...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
474
- ...(row.title !== null ? { title: String(row.title) } : {}),
475
- projectId: String(row.project_id),
476
- ...(row.current_linear_state !== null ? { currentLinearState: String(row.current_linear_state) } : {}),
477
- ...(row.session_state !== null ? { sessionState: String(row.session_state) } : {}),
478
- factoryState: String(row.factory_state ?? "delegated"),
479
- ...(row.waiting_reason !== null ? { waitingReason: String(row.waiting_reason) } : {}),
480
- ...(row.active_run_type !== null ? { activeRunType: String(row.active_run_type) } : {}),
481
- ...(row.latest_run_type !== null ? { latestRunType: String(row.latest_run_type) } : {}),
482
- ...(row.latest_run_status !== null ? { latestRunStatus: String(row.latest_run_status) } : {}),
483
- updatedAt: String(row.updated_at),
484
- }));
480
+ const items = rows.map((row) => {
481
+ const detachedActiveRun = row.active_run_type === null
482
+ && (row.latest_run_status === "queued" || row.latest_run_status === "running");
483
+ const activeRunType = row.active_run_type !== null
484
+ ? String(row.active_run_type)
485
+ : detachedActiveRun && row.latest_run_type !== null
486
+ ? String(row.latest_run_type)
487
+ : undefined;
488
+ const waitingReason = detachedActiveRun
489
+ ? derivePatchRelayWaitingReason({
490
+ activeRunId: 1,
491
+ factoryState: String(row.factory_state ?? "delegated"),
492
+ })
493
+ : row.waiting_reason !== null
494
+ ? String(row.waiting_reason)
495
+ : undefined;
496
+ return {
497
+ ...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
498
+ ...(row.title !== null ? { title: String(row.title) } : {}),
499
+ projectId: String(row.project_id),
500
+ ...(row.current_linear_state !== null ? { currentLinearState: String(row.current_linear_state) } : {}),
501
+ ...(row.session_state !== null ? { sessionState: detachedActiveRun ? "running" : String(row.session_state) } : {}),
502
+ factoryState: String(row.factory_state ?? "delegated"),
503
+ ...(waitingReason ? { waitingReason } : {}),
504
+ ...(activeRunType ? { activeRunType } : {}),
505
+ ...(row.latest_run_type !== null ? { latestRunType: String(row.latest_run_type) } : {}),
506
+ ...(row.latest_run_status !== null ? { latestRunStatus: String(row.latest_run_status) } : {}),
507
+ updatedAt: String(row.updated_at),
508
+ };
509
+ });
485
510
  return items.filter((item) => {
486
511
  if (options?.active && !item.activeRunType)
487
512
  return false;
@@ -0,0 +1,15 @@
1
+ function isActiveRunStatus(status) {
2
+ return status === "queued" || status === "running";
3
+ }
4
+ export function hasDetachedActiveLatestRun(params) {
5
+ return params.activeRunId === undefined
6
+ && params.latestRun !== undefined
7
+ && isActiveRunStatus(params.latestRun.status);
8
+ }
9
+ export function resolveEffectiveActiveRun(params) {
10
+ if (params.activeRun)
11
+ return params.activeRun;
12
+ if (params.latestRun && isActiveRunStatus(params.latestRun.status))
13
+ return params.latestRun;
14
+ return undefined;
15
+ }
@@ -4,6 +4,7 @@ import { resolveAuthoritativeLinearStopState } from "./linear-workflow.js";
4
4
  import { buildRunFailureActivity } from "./linear-session-reporting.js";
5
5
  import { getThreadTurns } from "./codex-thread-utils.js";
6
6
  import { resolveRecoverablePostRunState } from "./interrupted-run-recovery.js";
7
+ import { resolveEffectiveActiveRun } from "./effective-active-run.js";
7
8
  export class RunReconciler {
8
9
  db;
9
10
  logger;
@@ -33,6 +34,19 @@ export class RunReconciler {
33
34
  const { run, issue, recoveryLease } = params;
34
35
  const acquiredRecoveryLease = recoveryLease === true;
35
36
  let effectiveIssue = issue;
37
+ const effectiveActiveRun = resolveEffectiveActiveRun({
38
+ activeRun: issue.activeRunId === run.id ? run : undefined,
39
+ latestRun: run,
40
+ });
41
+ if (effectiveActiveRun?.id === run.id && issue.activeRunId !== run.id) {
42
+ effectiveIssue = this.withHeldLease(run.projectId, run.linearIssueId, () => this.db.issues.upsertIssue({
43
+ projectId: run.projectId,
44
+ linearIssueId: run.linearIssueId,
45
+ activeRunId: run.id,
46
+ ...(run.threadId ? { threadId: run.threadId } : {}),
47
+ })) ?? effectiveIssue;
48
+ this.logger.info({ issueKey: effectiveIssue.issueKey, runId: run.id, runType: run.runType }, "Reattached detached active run during reconciliation");
49
+ }
36
50
  if (!effectiveIssue.delegatedToPatchRelay) {
37
51
  const authority = await this.confirmDelegationAuthorityBeforeRelease(run, effectiveIssue);
38
52
  effectiveIssue = authority.issue;
@@ -2,6 +2,7 @@ import { parseGitHubFailureContext, summarizeGitHubFailureContext } from "./gith
2
2
  import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
3
3
  import { deriveIssueStatusNote } from "./status-note.js";
4
4
  import { isIssueSessionReadyForExecution } from "./issue-session.js";
5
+ import { hasDetachedActiveLatestRun } from "./effective-active-run.js";
5
6
  function shouldSuppressStatusNote(params) {
6
7
  if (!params.activeRunType && params.sessionState !== "running")
7
8
  return false;
@@ -155,11 +156,24 @@ export class TrackedIssueListQuery {
155
156
  const hasPendingSessionEvents = Number(row.pending_session_event_count ?? 0) > 0;
156
157
  const hasPendingWake = hasPendingSessionEvents
157
158
  || this.db.issueSessions.peekIssueSessionWake(String(row.project_id), String(row.linear_issue_id)) !== undefined;
159
+ const detachedActiveRun = hasDetachedActiveLatestRun({
160
+ activeRunId: row.active_run_type !== null ? 1 : undefined,
161
+ latestRun: row.latest_run_status !== null
162
+ ? { id: 0, status: String(row.latest_run_status) }
163
+ : undefined,
164
+ });
165
+ const effectiveActiveRunType = row.active_run_type !== null
166
+ ? String(row.active_run_type)
167
+ : detachedActiveRun && row.latest_run_type !== null
168
+ ? String(row.latest_run_type)
169
+ : undefined;
158
170
  const readyForExecution = isIssueSessionReadyForExecution({
159
- ...(typeof row.session_state === "string" ? { sessionState: String(row.session_state) } : {}),
171
+ ...(typeof row.session_state === "string"
172
+ ? { sessionState: detachedActiveRun ? "running" : String(row.session_state) }
173
+ : {}),
160
174
  factoryState: String(row.factory_state ?? "delegated"),
161
175
  ...(row.delegated_to_patchrelay !== null ? { delegatedToPatchRelay: Number(row.delegated_to_patchrelay) !== 0 } : {}),
162
- ...(row.active_run_type !== null ? { activeRunId: 1 } : {}),
176
+ ...((row.active_run_type !== null || detachedActiveRun) ? { activeRunId: 1 } : {}),
163
177
  blockedByCount,
164
178
  hasPendingWake,
165
179
  hasLegacyPendingRun: row.pending_run_type !== null && row.pending_run_type !== undefined,
@@ -177,9 +191,9 @@ export class TrackedIssueListQuery {
177
191
  const sessionSummary = typeof row.summary_text === "string" && row.summary_text.trim().length > 0
178
192
  ? row.summary_text
179
193
  : undefined;
180
- const waitingReason = sessionWaitingReason ?? derivePatchRelayWaitingReason({
194
+ const derivedWaitingReason = derivePatchRelayWaitingReason({
181
195
  ...(row.delegated_to_patchrelay !== null ? { delegatedToPatchRelay: Number(row.delegated_to_patchrelay) !== 0 } : {}),
182
- ...(row.active_run_type !== null ? { activeRunType: String(row.active_run_type) } : {}),
196
+ ...((row.active_run_type !== null || detachedActiveRun) ? { activeRunId: 1 } : {}),
183
197
  blockedByKeys,
184
198
  factoryState: String(row.factory_state ?? "delegated"),
185
199
  ...(row.pending_run_type !== null ? { pendingRunType: String(row.pending_run_type) } : {}),
@@ -192,6 +206,7 @@ export class TrackedIssueListQuery {
192
206
  ...(row.last_blocking_review_head_sha !== null ? { lastBlockingReviewHeadSha: String(row.last_blocking_review_head_sha) } : {}),
193
207
  ...(row.last_github_failure_check_name !== null ? { latestFailureCheckName: String(row.last_github_failure_check_name) } : {}),
194
208
  });
209
+ const waitingReason = detachedActiveRun ? derivedWaitingReason : sessionWaitingReason ?? derivedWaitingReason;
195
210
  const latestRun = row.latest_run_type !== null && row.latest_run_status !== null
196
211
  ? {
197
212
  id: 0,
@@ -222,29 +237,39 @@ export class TrackedIssueListQuery {
222
237
  waitingReason,
223
238
  }) ?? waitingReason;
224
239
  const statusNoteForReturn = shouldSuppressStatusNote({
225
- activeRunType: row.active_run_type,
226
- sessionState: row.session_state,
240
+ activeRunType: effectiveActiveRunType,
241
+ sessionState: detachedActiveRun ? "running" : row.session_state,
227
242
  statusNote: statusNoteCandidate,
228
243
  })
229
244
  ? undefined
230
245
  : statusNoteCandidate;
231
- const completionCheckActive = typeof row.active_completion_check_thread_id === "string"
232
- && row.active_completion_check_thread_id.length > 0
233
- && row.active_completion_check_outcome === null
234
- && row.active_run_type !== null;
246
+ const activeCompletionCheckThreadId = row.active_run_type !== null
247
+ ? row.active_completion_check_thread_id
248
+ : detachedActiveRun
249
+ ? row.latest_run_completion_check_thread_id
250
+ : null;
251
+ const activeCompletionCheckOutcome = row.active_run_type !== null
252
+ ? row.active_completion_check_outcome
253
+ : detachedActiveRun
254
+ ? row.latest_run_completion_check_outcome
255
+ : null;
256
+ const completionCheckActive = typeof activeCompletionCheckThreadId === "string"
257
+ && activeCompletionCheckThreadId.length > 0
258
+ && activeCompletionCheckOutcome === null
259
+ && effectiveActiveRunType !== undefined;
235
260
  return {
236
261
  ...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
237
262
  ...(row.title !== null ? { title: String(row.title) } : {}),
238
263
  ...(statusNoteForReturn ? { statusNote: statusNoteForReturn } : {}),
239
264
  projectId: String(row.project_id),
240
265
  delegatedToPatchRelay: row.delegated_to_patchrelay === null ? true : Number(row.delegated_to_patchrelay) !== 0,
241
- ...(row.session_state !== null ? { sessionState: String(row.session_state) } : {}),
266
+ ...(row.session_state !== null ? { sessionState: detachedActiveRun ? "running" : String(row.session_state) } : {}),
242
267
  factoryState: String(row.factory_state ?? "delegated"),
243
268
  blockedByCount,
244
269
  blockedByKeys,
245
270
  readyForExecution,
246
271
  ...(row.current_linear_state !== null ? { currentLinearState: String(row.current_linear_state) } : {}),
247
- ...(row.active_run_type !== null ? { activeRunType: String(row.active_run_type) } : {}),
272
+ ...(effectiveActiveRunType ? { activeRunType: effectiveActiveRunType } : {}),
248
273
  ...(row.pending_run_type !== null ? { pendingRunType: String(row.pending_run_type) } : {}),
249
274
  ...(row.latest_run_type !== null ? { latestRunType: String(row.latest_run_type) } : {}),
250
275
  ...(row.latest_run_status !== null ? { latestRunStatus: String(row.latest_run_status) } : {}),
@@ -2,6 +2,7 @@ import { parseGitHubFailureContext } from "./github-failure-context.js";
2
2
  import { isIssueSessionReadyForExecution } from "./issue-session.js";
3
3
  import { deriveIssueStatusNote } from "./status-note.js";
4
4
  import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
5
+ import { hasDetachedActiveLatestRun, resolveEffectiveActiveRun } from "./effective-active-run.js";
5
6
  export function isResolvedLinearState(stateType, stateName) {
6
7
  return stateType === "completed" || stateName?.trim().toLowerCase() === "done";
7
8
  }
@@ -9,9 +10,17 @@ export function buildTrackedIssueRecord(params) {
9
10
  const unresolvedBlockedBy = params.blockedBy.filter((entry) => !isResolvedLinearState(entry.blockerCurrentLinearStateType, entry.blockerCurrentLinearState));
10
11
  const failureContext = parseGitHubFailureContext(params.issue.lastGitHubFailureContextJson);
11
12
  const blockedByKeys = unresolvedBlockedBy.map((entry) => entry.blockerIssueKey ?? entry.blockerLinearIssueId);
13
+ const effectiveActiveRun = resolveEffectiveActiveRun({
14
+ activeRun: params.issue.activeRunId !== undefined && params.latestRun?.id === params.issue.activeRunId ? params.latestRun : undefined,
15
+ latestRun: params.latestRun,
16
+ });
17
+ const detachedActiveRun = hasDetachedActiveLatestRun({
18
+ activeRunId: params.issue.activeRunId,
19
+ latestRun: params.latestRun,
20
+ });
12
21
  const waitingReason = derivePatchRelayWaitingReason({
13
22
  delegatedToPatchRelay: params.issue.delegatedToPatchRelay,
14
- ...(params.issue.activeRunId !== undefined ? { activeRunId: params.issue.activeRunId } : {}),
23
+ ...(effectiveActiveRun ? { activeRunId: effectiveActiveRun.id } : {}),
15
24
  blockedByKeys,
16
25
  factoryState: params.issue.factoryState,
17
26
  pendingRunType: params.issue.pendingRunType,
@@ -33,11 +42,9 @@ export function buildTrackedIssueRecord(params) {
33
42
  blockedByKeys,
34
43
  waitingReason,
35
44
  });
36
- const completionCheckActive = Boolean(params.issue.activeRunId !== undefined
37
- && params.latestRun?.id === params.issue.activeRunId
38
- && params.latestRun.status === "running"
39
- && params.latestRun.completionCheckThreadId
40
- && !params.latestRun.completionCheckOutcome);
45
+ const completionCheckActive = Boolean(effectiveActiveRun?.status === "running"
46
+ && effectiveActiveRun.completionCheckThreadId
47
+ && !effectiveActiveRun.completionCheckOutcome);
41
48
  return {
42
49
  id: params.issue.id,
43
50
  projectId: params.issue.projectId,
@@ -61,7 +68,7 @@ export function buildTrackedIssueRecord(params) {
61
68
  sessionState: params.session?.sessionState,
62
69
  factoryState: params.issue.factoryState,
63
70
  delegatedToPatchRelay: params.issue.delegatedToPatchRelay,
64
- activeRunId: params.issue.activeRunId,
71
+ ...(effectiveActiveRun ? { activeRunId: effectiveActiveRun.id } : {}),
65
72
  blockedByCount: unresolvedBlockedBy.length,
66
73
  hasPendingWake: params.hasPendingWake,
67
74
  hasLegacyPendingRun: params.issue.pendingRunType !== undefined,
@@ -79,8 +86,9 @@ export function buildTrackedIssueRecord(params) {
79
86
  ...(failureContext?.summary ? { latestFailureSummary: failureContext.summary } : {}),
80
87
  ...(waitingReason ? { waitingReason } : {}),
81
88
  ...(completionCheckActive ? { completionCheckActive } : {}),
82
- ...(params.issue.activeRunId !== undefined ? { activeRunId: params.issue.activeRunId } : {}),
89
+ ...(effectiveActiveRun ? { activeRunId: effectiveActiveRun.id } : {}),
83
90
  ...(params.issue.agentSessionId ? { activeAgentSessionId: params.issue.agentSessionId } : {}),
91
+ ...(detachedActiveRun && params.session?.sessionState ? { sessionState: "running" } : {}),
84
92
  updatedAt: params.issue.updatedAt,
85
93
  };
86
94
  }
@@ -1,5 +1,6 @@
1
1
  import { buildTrackedIssueRecord } from "./tracked-issue-projector.js";
2
2
  import { deriveIssueSessionState, isIssueSessionReadyForExecution } from "./issue-session.js";
3
+ import { resolveEffectiveActiveRun } from "./effective-active-run.js";
3
4
  export class TrackedIssueQuery {
4
5
  issues;
5
6
  issueSessions;
@@ -55,7 +56,10 @@ export class TrackedIssueQuery {
55
56
  if (!issue)
56
57
  return undefined;
57
58
  const tracked = this.issueToTrackedIssue(issue);
58
- const activeRun = issue.activeRunId ? this.runs.getRunById(issue.activeRunId) : undefined;
59
+ const activeRun = resolveEffectiveActiveRun({
60
+ activeRun: issue.activeRunId ? this.runs.getRunById(issue.activeRunId) : undefined,
61
+ latestRun: this.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId),
62
+ });
59
63
  return {
60
64
  issue: tracked,
61
65
  ...(activeRun ? { activeRun } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.49.0",
3
+ "version": "0.49.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {