patchrelay 0.8.2 → 0.8.3

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.8.2",
4
- "commit": "bae4a3fbeb97",
5
- "builtAt": "2026-03-18T21:53:19.259Z"
3
+ "version": "0.8.3",
4
+ "commit": "ef05604cd432",
5
+ "builtAt": "2026-03-18T23:26:09.415Z"
6
6
  }
@@ -200,7 +200,7 @@ export class IssueWorkflowCoordinator {
200
200
  });
201
201
  }
202
202
  const workspace = this.authoritativeLedger.getWorkspaceOwnership(stageRun.workspaceId);
203
- if (workspace) {
203
+ if (workspace && workspace.currentRunLeaseId === params.stageRunId) {
204
204
  this.authoritativeLedger.upsertWorkspaceOwnership({
205
205
  projectId: stageRun.projectId,
206
206
  linearIssueId: stageRun.linearIssueId,
@@ -8,7 +8,6 @@ import { resolveDefaultTransitionTarget, transitionTargetAllowed } from "./workf
8
8
  import { buildFailedStageReport, buildPendingMaterializationThread, buildStageReport, countEventMethods, extractStageSummary, extractTurnId, resolveStageRunStatus, summarizeCurrentThread, } from "./stage-reporting.js";
9
9
  import { StageLifecyclePublisher } from "./stage-lifecycle-publisher.js";
10
10
  import { StageTurnInputDispatcher } from "./stage-turn-input-dispatcher.js";
11
- const MAX_AUTOMATIC_TRANSITION_ATTEMPTS = 3;
12
11
  export class ServiceStageFinalizer {
13
12
  config;
14
13
  stores;
@@ -153,6 +152,7 @@ export class ServiceStageFinalizer {
153
152
  const report = buildStageReport(finalizedStageRun, issue, thread, countEventMethods(this.stores.stageEvents.listThreadEvents(stageRun.id)));
154
153
  this.runAtomically(() => {
155
154
  this.finishLedgerRun(stageRun.projectId, stageRun.linearIssueId, "completed", {
155
+ stageRunId: stageRun.id,
156
156
  threadId: params.threadId,
157
157
  ...(params.turnId ? { turnId: params.turnId } : {}),
158
158
  nextLifecycleStatus: params.nextLifecycleStatus ?? (issue.desiredStage ? "queued" : "completed"),
@@ -171,6 +171,7 @@ export class ServiceStageFinalizer {
171
171
  failStageRun(stageRun, threadId, message, options) {
172
172
  this.runAtomically(() => {
173
173
  this.finishLedgerRun(stageRun.projectId, stageRun.linearIssueId, "failed", {
174
+ stageRunId: stageRun.id,
174
175
  threadId,
175
176
  ...(options?.turnId ? { turnId: options.turnId } : {}),
176
177
  failureReason: message,
@@ -315,9 +316,19 @@ export class ServiceStageFinalizer {
315
316
  await this.routeStageToHumanNeeded(project, stageRun, linearIssue, `PatchRelay received ${nextTarget} as the next stage again and needs a human to confirm the intended loop.`);
316
317
  return;
317
318
  }
318
- const priorAttempts = this.countPriorTransitionAttempts(stageRun.projectId, stageRun.linearIssueId, stageRun.stage, nextTarget);
319
- if (priorAttempts >= MAX_AUTOMATIC_TRANSITION_ATTEMPTS) {
320
- await this.routeStageToHumanNeeded(project, stageRun, linearIssue, `PatchRelay hit the automatic continuation limit for ${stageRun.stage} -> ${nextTarget}.`);
319
+ if (this.isTransitionAlreadyInFlight(stageRun, nextTarget)) {
320
+ this.feed?.publish({
321
+ level: "info",
322
+ kind: "workflow",
323
+ issueKey: refreshedIssue.issueKey,
324
+ projectId: stageRun.projectId,
325
+ stage: stageRun.stage,
326
+ ...(refreshedIssue.selectedWorkflowId ? { workflowId: refreshedIssue.selectedWorkflowId } : {}),
327
+ nextStage: nextTarget,
328
+ status: "transition_in_progress",
329
+ summary: `${nextTarget} is already queued or running`,
330
+ detail: `PatchRelay kept ${stageRun.stage} completion from re-queueing ${nextTarget}.`,
331
+ });
321
332
  return;
322
333
  }
323
334
  this.feed?.publish({
@@ -360,19 +371,16 @@ export class ServiceStageFinalizer {
360
371
  }
361
372
  return resolveDefaultTransitionTarget(project, stageRun.stage, workflowDefinitionId) ?? "human_needed";
362
373
  }
363
- countPriorTransitionAttempts(projectId, linearIssueId, currentStage, nextTarget) {
364
- const stageHistory = this.stores.issueWorkflows
365
- .listStageRunsForIssue(projectId, linearIssueId)
366
- .filter((stageRun) => stageRun.status === "completed");
367
- let count = 0;
368
- for (let index = 0; index < stageHistory.length - 1; index += 1) {
369
- const current = stageHistory[index];
370
- const next = stageHistory[index + 1];
371
- if (current?.stage === currentStage && next?.stage === nextTarget) {
372
- count += 1;
373
- }
374
+ isTransitionAlreadyInFlight(stageRun, nextTarget) {
375
+ const refreshedIssue = this.stores.issueWorkflows.getTrackedIssue(stageRun.projectId, stageRun.linearIssueId);
376
+ if (!refreshedIssue) {
377
+ return false;
378
+ }
379
+ if (refreshedIssue.desiredStage === nextTarget) {
380
+ return true;
374
381
  }
375
- return count;
382
+ const activeStageRun = this.resolveActiveStageRun(refreshedIssue);
383
+ return activeStageRun !== undefined && activeStageRun.id !== stageRun.id && activeStageRun.stage === nextTarget;
376
384
  }
377
385
  async routeStageToHumanNeeded(project, stageRun, linearIssue, reason) {
378
386
  const linear = await this.linearProvider.forProject(stageRun.projectId);
@@ -408,29 +416,41 @@ export class ServiceStageFinalizer {
408
416
  }
409
417
  finishLedgerRun(projectId, linearIssueId, status, params) {
410
418
  const issueControl = this.stores.issueControl.getIssueControl(projectId, linearIssueId);
411
- if (!issueControl?.activeRunLeaseId) {
419
+ const targetRunLeaseId = params.stageRunId ?? issueControl?.activeRunLeaseId;
420
+ if (!targetRunLeaseId) {
421
+ return;
422
+ }
423
+ const targetRunLease = this.stores.runLeases.getRunLease(targetRunLeaseId);
424
+ if (!targetRunLease) {
412
425
  return;
413
426
  }
414
427
  this.stores.runLeases.finishRunLease({
415
- runLeaseId: issueControl.activeRunLeaseId,
428
+ runLeaseId: targetRunLeaseId,
416
429
  status,
417
430
  ...(params.threadId ? { threadId: params.threadId } : {}),
418
431
  ...(params.turnId ? { turnId: params.turnId } : {}),
419
432
  ...(params.failureReason ? { failureReason: params.failureReason } : {}),
420
433
  });
421
- if (issueControl.activeWorkspaceOwnershipId !== undefined) {
422
- const workspace = this.stores.workspaceOwnership.getWorkspaceOwnership(issueControl.activeWorkspaceOwnershipId);
434
+ if (targetRunLease.workspaceOwnershipId !== undefined) {
435
+ const workspace = this.stores.workspaceOwnership.getWorkspaceOwnership(targetRunLease.workspaceOwnershipId);
423
436
  if (workspace) {
424
437
  this.stores.workspaceOwnership.upsertWorkspaceOwnership({
425
438
  projectId,
426
439
  linearIssueId,
427
440
  branchName: workspace.branchName,
428
441
  worktreePath: workspace.worktreePath,
429
- status: status === "completed" ? "active" : "paused",
430
- currentRunLeaseId: null,
442
+ status: workspace.currentRunLeaseId === targetRunLeaseId ? (status === "completed" ? "active" : "paused") : workspace.status,
443
+ ...(workspace.currentRunLeaseId === targetRunLeaseId
444
+ ? { currentRunLeaseId: null }
445
+ : workspace.currentRunLeaseId !== undefined
446
+ ? { currentRunLeaseId: workspace.currentRunLeaseId }
447
+ : {}),
431
448
  });
432
449
  }
433
450
  }
451
+ if (!issueControl?.activeRunLeaseId || issueControl.activeRunLeaseId !== targetRunLeaseId) {
452
+ return;
453
+ }
434
454
  this.stores.issueControl.upsertIssueControl({
435
455
  projectId,
436
456
  linearIssueId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {