patchrelay 0.36.14 → 0.36.16

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.36.14",
4
- "commit": "cf7280bff401",
5
- "builtAt": "2026-04-10T04:01:33.969Z"
3
+ "version": "0.36.16",
4
+ "commit": "f8ad93bd7a05",
5
+ "builtAt": "2026-04-10T04:39:43.765Z"
6
6
  }
@@ -103,6 +103,10 @@ export class InterruptedRunRecovery {
103
103
  await this.handleInterruptedRequestedChangesRun(run, issue);
104
104
  return;
105
105
  }
106
+ if (run.runType === "implementation" && !issue.prNumber) {
107
+ await this.handleInterruptedImplementationRun(run, issue);
108
+ return;
109
+ }
106
110
  const recoveredState = resolveRecoverablePostRunState(this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? issue);
107
111
  this.failRunAndClear(run, "Codex turn was interrupted", recoveredState);
108
112
  await this.restoreIdleWorktree(issue);
@@ -124,6 +128,47 @@ export class InterruptedRunRecovery {
124
128
  void this.linearSync.syncSession(failedIssue, { activeRunType: run.runType });
125
129
  this.releaseLease(run.projectId, run.linearIssueId);
126
130
  }
131
+ async handleInterruptedImplementationRun(run, issue) {
132
+ const interruptedMessage = "Implementation run was interrupted before PatchRelay could publish a PR";
133
+ this.failRunAndClear(run, "Codex turn was interrupted", "delegated");
134
+ await this.restoreIdleWorktree(issue);
135
+ const refreshedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? issue;
136
+ this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(run.projectId, run.linearIssueId, {
137
+ projectId: run.projectId,
138
+ linearIssueId: run.linearIssueId,
139
+ eventType: "delegated",
140
+ dedupeKey: `interrupted_implementation:implementation:${run.linearIssueId}`,
141
+ });
142
+ if (!this.db.issueSessions.peekIssueSessionWake(run.projectId, run.linearIssueId)) {
143
+ const failedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? refreshedIssue;
144
+ this.feed?.publish({
145
+ level: "error",
146
+ kind: "workflow",
147
+ issueKey: issue.issueKey,
148
+ projectId: run.projectId,
149
+ stage: run.runType,
150
+ status: "escalated",
151
+ summary: interruptedMessage,
152
+ });
153
+ void this.linearSync.emitActivity(failedIssue, buildRunFailureActivity(run.runType, interruptedMessage));
154
+ void this.linearSync.syncSession(failedIssue, { activeRunType: run.runType });
155
+ this.releaseLease(run.projectId, run.linearIssueId);
156
+ return;
157
+ }
158
+ this.feed?.publish({
159
+ level: "warn",
160
+ kind: "workflow",
161
+ issueKey: issue.issueKey,
162
+ projectId: run.projectId,
163
+ stage: run.runType,
164
+ status: "retry_queued",
165
+ summary: "Implementation run was interrupted; PatchRelay will retry automatically",
166
+ });
167
+ const recoveredIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? refreshedIssue;
168
+ void this.linearSync.syncSession(recoveredIssue, { activeRunType: run.runType });
169
+ this.enqueueIssue(run.projectId, run.linearIssueId);
170
+ this.releaseLease(run.projectId, run.linearIssueId);
171
+ }
127
172
  async handleInterruptedRequestedChangesRun(run, issue) {
128
173
  const freshIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? issue;
129
174
  const refreshedIssue = await this.completionPolicy.refreshIssueAfterReactivePublish(run, freshIssue);
@@ -254,7 +254,7 @@ export class LinearSessionSync {
254
254
  const previous = this.agentMessageBuffers.get(messageKey) ?? "";
255
255
  const next = `${previous}${delta}`;
256
256
  this.agentMessageBuffers.set(messageKey, next);
257
- const sentence = extractFirstSentence(next);
257
+ const sentence = extractFirstCompletedSentence(next);
258
258
  if (!sentence)
259
259
  return undefined;
260
260
  this.agentMessageProgressPublished.add(messageKey);
@@ -522,6 +522,13 @@ function extractFirstSentence(text) {
522
522
  const match = sanitized.match(/^(.+?[.!?])(?:\s|$)/);
523
523
  return truncateProgressText((match?.[1] ?? sanitized).trim(), MAX_PROGRESS_TEXT_LENGTH);
524
524
  }
525
+ function extractFirstCompletedSentence(text) {
526
+ const sanitized = sanitizeOperatorFacingText(text)?.replace(/\s+/g, " ").trim();
527
+ if (!sanitized)
528
+ return undefined;
529
+ const match = sanitized.match(/^(.+?[.!?])(?:\s|$)/);
530
+ return match?.[1] ? truncateProgressText(match[1].trim(), MAX_PROGRESS_TEXT_LENGTH) : undefined;
531
+ }
525
532
  function summarizeProgressSentence(text) {
526
533
  const summary = extractFirstSentence(text);
527
534
  if (!summary)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.36.14",
3
+ "version": "0.36.16",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {