patchrelay 0.51.1 → 0.51.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.51.1",
4
- "commit": "0b16fe78f8c9",
5
- "builtAt": "2026-04-21T22:06:12.939Z"
3
+ "version": "0.51.3",
4
+ "commit": "7bb8f3bef8bf",
5
+ "builtAt": "2026-04-22T04:21:59.470Z"
6
6
  }
package/dist/cli/data.js CHANGED
@@ -70,6 +70,9 @@ function summarizeRun(run) {
70
70
  : completionCheck.summary;
71
71
  }
72
72
  const summary = parseObjectJson(run.summaryJson);
73
+ if (typeof summary?.publicationRecapSummary === "string" && summary.publicationRecapSummary.trim()) {
74
+ return summary.publicationRecapSummary.trim();
75
+ }
73
76
  if (typeof summary?.latestAssistantMessage === "string" && summary.latestAssistantMessage.trim()) {
74
77
  return summary.latestAssistantMessage.trim();
75
78
  }
@@ -9,6 +9,14 @@ const COMPLETION_CHECK_DEVELOPER_INSTRUCTIONS = [
9
9
  "Use only the prior thread context and the facts in the current prompt.",
10
10
  "Return only the requested JSON object.",
11
11
  ].join("\n");
12
+ const PUBLICATION_RECAP_DEVELOPER_INSTRUCTIONS = [
13
+ "You are PatchRelay's publication recap helper.",
14
+ "This is a read-only follow-up used only to produce one concise Linear-visible summary for a successful run.",
15
+ "Keep reasoning light and concise.",
16
+ "Do not run commands, do not call tools, do not edit files, and do not inspect or modify the repository.",
17
+ "Use only the prior thread context and the facts in the current prompt.",
18
+ "Return only the requested JSON object.",
19
+ ].join("\n");
12
20
  export function resolveCodexAppServerLaunch(config) {
13
21
  if (!config.sourceBashrc) {
14
22
  return {
@@ -210,6 +218,14 @@ export class CodexAppServerClient extends EventEmitter {
210
218
  developerInstructions: COMPLETION_CHECK_DEVELOPER_INSTRUCTIONS,
211
219
  });
212
220
  }
221
+ async forkThreadForPublicationRecap(threadId) {
222
+ return await this.forkThread(threadId, tmpdir(), {
223
+ approvalPolicy: "never",
224
+ sandboxMode: "read-only",
225
+ reasoningEffort: "low",
226
+ developerInstructions: PUBLICATION_RECAP_DEVELOPER_INSTRUCTIONS,
227
+ });
228
+ }
213
229
  async startTurn(options) {
214
230
  const response = (await this.sendRequest("turn/start", {
215
231
  threadId: options.threadId,
@@ -149,6 +149,9 @@ export function extractLatestAssistantSummary(run) {
149
149
  if (run.summaryJson) {
150
150
  try {
151
151
  const parsed = JSON.parse(run.summaryJson);
152
+ if (typeof parsed.publicationRecapSummary === "string" && parsed.publicationRecapSummary.trim()) {
153
+ return sanitizeOperatorFacingText(parsed.publicationRecapSummary);
154
+ }
152
155
  if (typeof parsed.latestAssistantMessage === "string" && parsed.latestAssistantMessage.trim()) {
153
156
  return sanitizeOperatorFacingText(parsed.latestAssistantMessage);
154
157
  }
@@ -1,4 +1,4 @@
1
- import { shouldSyncVisibleIssueComment, syncVisibleStatusComment, } from "./linear-status-comment-sync.js";
1
+ import { collapseVisibleStatusComment, shouldSyncVisibleIssueComment, syncVisibleStatusComment, } from "./linear-status-comment-sync.js";
2
2
  import { LinearAgentSessionClient } from "./linear-agent-session-client.js";
3
3
  import { LinearProgressReporter } from "./linear-progress-reporter.js";
4
4
  import { syncActiveWorkflowState } from "./linear-workflow-state-sync.js";
@@ -49,6 +49,13 @@ export class LinearSessionSync {
49
49
  ...(options ? { options } : {}),
50
50
  });
51
51
  }
52
+ else if (syncedIssue.statusCommentId) {
53
+ await collapseVisibleStatusComment({
54
+ issue: syncedIssue,
55
+ linear,
56
+ logger: this.logger,
57
+ });
58
+ }
52
59
  }
53
60
  catch (error) {
54
61
  const msg = error instanceof Error ? error.message : String(error);
@@ -25,8 +25,40 @@ export async function syncVisibleStatusComment(params) {
25
25
  logger.warn({ issueKey: issue.issueKey, error: msg }, "Failed to sync Linear status comment");
26
26
  }
27
27
  }
28
- export function shouldSyncVisibleIssueComment(_issue, _hasAgentSession) {
29
- return true;
28
+ export function shouldSyncVisibleIssueComment(issue, hasAgentSession) {
29
+ if (!hasAgentSession) {
30
+ return true;
31
+ }
32
+ if (!issue.delegatedToPatchRelay) {
33
+ return true;
34
+ }
35
+ if (issue.sessionState === "waiting_input" || issue.factoryState === "awaiting_input") {
36
+ return true;
37
+ }
38
+ if (issue.sessionState === "failed" || issue.factoryState === "failed" || issue.factoryState === "escalated") {
39
+ return true;
40
+ }
41
+ if (issue.factoryState === "done") {
42
+ return issue.prState !== "merged";
43
+ }
44
+ return false;
45
+ }
46
+ export async function collapseVisibleStatusComment(params) {
47
+ const { issue, linear, logger } = params;
48
+ if (!issue.statusCommentId) {
49
+ return;
50
+ }
51
+ try {
52
+ await linear.upsertIssueComment({
53
+ issueId: issue.linearIssueId,
54
+ commentId: issue.statusCommentId,
55
+ body: renderCollapsedStatusComment(),
56
+ });
57
+ }
58
+ catch (error) {
59
+ const msg = error instanceof Error ? error.message : String(error);
60
+ logger.warn({ issueId: issue.linearIssueId, error: msg }, "Failed to collapse Linear status comment");
61
+ }
30
62
  }
31
63
  function renderStatusComment(db, issue, trackedIssue, options) {
32
64
  const activeRun = issue.activeRunId ? db.runs.getRunById(issue.activeRunId) : undefined;
@@ -97,21 +129,18 @@ function renderStatusComment(db, issue, trackedIssue, options) {
97
129
  : `PR: ${linkedLabel}`;
98
130
  lines.push("", prLine);
99
131
  }
100
- if (latestRun) {
101
- lines.push("", `Latest run: ${formatLatestRun(latestRun)}`);
102
- if (latestRun.failureReason) {
103
- lines.push("", `Failure: ${latestRun.failureReason}`);
104
- }
105
- if (completionCheck && completionCheck.outcome !== "needs_input" && completionCheck.summary !== statusNote) {
106
- lines.push("", `Completion check: ${completionCheck.summary}`);
107
- }
108
- }
109
132
  if (issue.lastGitHubFailureCheckName && (issue.factoryState === "repairing_ci" || issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure")) {
110
133
  lines.push("", `Latest failing check: ${issue.lastGitHubFailureCheckName}`);
111
134
  }
112
- lines.push("", "_PatchRelay updates this comment as it works. Review and merge remain downstream._");
113
135
  return lines.join("\n");
114
136
  }
137
+ function renderCollapsedStatusComment() {
138
+ return [
139
+ "## PatchRelay status",
140
+ "",
141
+ "Live status is in the agent session and activity feed. This comment is reused only when PatchRelay needs human input or intervention.",
142
+ ].join("\n");
143
+ }
115
144
  function statusHeadline(issue, activeRunType) {
116
145
  const prContext = derivePrDisplayContext(issue);
117
146
  if (activeRunType) {
@@ -193,10 +222,6 @@ function statusHeadline(issue, activeRunType) {
193
222
  return humanize(issue.factoryState);
194
223
  }
195
224
  }
196
- function formatLatestRun(run) {
197
- const at = run.endedAt ?? run.startedAt;
198
- return `${humanize(run.runType)} ${run.status} at ${at}`;
199
- }
200
225
  function humanize(value) {
201
226
  return value.replaceAll("_", " ");
202
227
  }
@@ -0,0 +1,113 @@
1
+ import { getThreadTurns } from "./codex-thread-utils.js";
2
+ import { extractLatestAssistantSummary } from "./issue-session-events.js";
3
+ import { sanitizeOperatorFacingText } from "./presentation-text.js";
4
+ import { extractFirstJsonObject, safeJsonParse } from "./utils.js";
5
+ const PUBLICATION_RECAP_TIMEOUT_MS = 45_000;
6
+ const PUBLICATION_RECAP_POLL_MS = 1_000;
7
+ export class PublicationRecapService {
8
+ codex;
9
+ logger;
10
+ constructor(codex, logger) {
11
+ this.codex = codex;
12
+ this.logger = logger;
13
+ }
14
+ async run(params) {
15
+ const threadId = params.run.threadId;
16
+ if (!threadId) {
17
+ throw new Error("Publication recap could not run because the main thread is missing.");
18
+ }
19
+ const fork = await this.codex.forkThreadForPublicationRecap(threadId);
20
+ const turn = await this.codex.startTurn({
21
+ threadId: fork.id,
22
+ ...(fork.cwd ? { cwd: fork.cwd } : {}),
23
+ input: buildPublicationRecapPrompt(params),
24
+ });
25
+ const completedThread = await this.waitForTurn(fork.id, turn.turnId);
26
+ const completedTurn = getThreadTurns(completedThread).find((entry) => entry.id === turn.turnId);
27
+ const latestMessage = completedTurn?.items
28
+ .filter((item) => item.type === "agentMessage")
29
+ .at(-1)?.text;
30
+ const parsed = parsePublicationRecapResult(latestMessage);
31
+ if (!parsed) {
32
+ this.logger.warn({ runId: params.run.id, issueKey: params.issue.issueKey, threadId: fork.id, turnId: turn.turnId }, "Publication recap returned invalid JSON");
33
+ throw new Error("Publication recap returned an invalid result.");
34
+ }
35
+ return {
36
+ ...parsed,
37
+ threadId: fork.id,
38
+ turnId: turn.turnId,
39
+ };
40
+ }
41
+ async waitForTurn(threadId, turnId) {
42
+ const deadline = Date.now() + PUBLICATION_RECAP_TIMEOUT_MS;
43
+ while (Date.now() < deadline) {
44
+ const thread = await this.codex.readThread(threadId, true);
45
+ const turn = getThreadTurns(thread).find((entry) => entry.id === turnId);
46
+ if (turn?.status === "completed") {
47
+ return thread;
48
+ }
49
+ if (turn?.status === "failed" || turn?.status === "interrupted") {
50
+ throw new Error(`Publication recap turn ${turnId} ended with status ${turn.status}`);
51
+ }
52
+ await new Promise((resolve) => setTimeout(resolve, PUBLICATION_RECAP_POLL_MS));
53
+ }
54
+ throw new Error(`Publication recap timed out after ${PUBLICATION_RECAP_TIMEOUT_MS}ms`);
55
+ }
56
+ }
57
+ function buildPublicationRecapPrompt(params) {
58
+ const latestSummary = params.facts?.latestAssistantSummary
59
+ ? sanitizeOperatorFacingText(params.facts.latestAssistantSummary)
60
+ : extractLatestAssistantSummary(params.run);
61
+ return [
62
+ "PatchRelay publication recap",
63
+ "",
64
+ "The main task run succeeded.",
65
+ "This is a read-only follow-up used only to produce one concise Linear-visible recap for that successful run.",
66
+ "Do not run commands, call tools, edit files, or inspect the repository.",
67
+ "Use only the prior thread context and the facts in this prompt.",
68
+ "Return exactly one JSON object and no extra prose.",
69
+ "",
70
+ "Schema:",
71
+ '{',
72
+ ' "summary": "one short sentence, max 30 words"',
73
+ '}',
74
+ "",
75
+ "Writing rules:",
76
+ "- Focus on what this session chunk achieved.",
77
+ "- Mention the wake reason only when it makes the change clearer, for example requested changes, a failing CI check, or a merge queue incident.",
78
+ "- Do not list touched files, test commands, branch names, commit SHAs, or internal process details.",
79
+ "- Do not say that you reviewed files or ran checks unless that is the only meaningful achievement.",
80
+ "- For implementation runs, summarize the delivered user-facing or system-facing change.",
81
+ "- For review-fix runs, summarize the concern that was addressed and imply that a newer head was published.",
82
+ "- For CI repair runs, summarize the failure that was fixed if known.",
83
+ "- For queue repair runs, summarize the queue or merge issue that was resolved if known.",
84
+ "",
85
+ "Facts:",
86
+ `- Issue: ${params.issue.issueKey ?? params.issue.linearIssueId}`,
87
+ ...(params.issue.title ? [`- Title: ${params.issue.title}`] : []),
88
+ `- Run type: ${params.run.runType}`,
89
+ ...(params.facts?.postRunState ? [`- Post-run state: ${params.facts.postRunState}`] : []),
90
+ ...(params.facts?.wakeReason ? [`- Wake reason: ${params.facts.wakeReason}`] : []),
91
+ ...(params.facts?.prNumber !== undefined ? [`- PR number: ${params.facts.prNumber}`] : []),
92
+ ...(params.facts?.reviewerName ? [`- Reviewer: ${params.facts.reviewerName}`] : []),
93
+ ...(params.facts?.reviewSummary ? [`- Review summary: ${sanitizeOperatorFacingText(params.facts.reviewSummary)}`] : []),
94
+ ...(params.facts?.failingCheckName ? [`- Failing check: ${params.facts.failingCheckName}`] : []),
95
+ ...(params.facts?.failureSummary ? [`- Failure summary: ${sanitizeOperatorFacingText(params.facts.failureSummary)}`] : []),
96
+ ...(params.facts?.queueIncidentSummary ? [`- Queue incident: ${sanitizeOperatorFacingText(params.facts.queueIncidentSummary)}`] : []),
97
+ ...(latestSummary ? [`- Latest assistant summary: ${latestSummary}`] : []),
98
+ ...(params.run.failureReason ? [`- Failure reason: ${sanitizeOperatorFacingText(params.run.failureReason)}`] : []),
99
+ ...(params.issue.description ? ["", "Issue description:", params.issue.description] : []),
100
+ ].join("\n");
101
+ }
102
+ function parsePublicationRecapResult(text) {
103
+ const raw = sanitizeOperatorFacingText(text);
104
+ if (!raw)
105
+ return undefined;
106
+ const candidate = safeJsonParse(raw) ?? safeJsonParse(extractFirstJsonObject(raw) ?? "");
107
+ if (!candidate)
108
+ return undefined;
109
+ const summary = typeof candidate.summary === "string" ? sanitizeOperatorFacingText(candidate.summary) : undefined;
110
+ if (!summary)
111
+ return undefined;
112
+ return { summary };
113
+ }
@@ -2,6 +2,30 @@ import { buildStageReport, countEventMethods } from "./run-reporting.js";
2
2
  import { buildRunCompletedActivity, buildRunFailureActivity } from "./linear-session-reporting.js";
3
3
  import { handleNoPrCompletionCheck } from "./no-pr-completion-check.js";
4
4
  import { resolveCompletedRunState } from "./run-completion-policy.js";
5
+ function parseEventJson(eventJson) {
6
+ if (!eventJson)
7
+ return undefined;
8
+ try {
9
+ const parsed = JSON.parse(eventJson);
10
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ }
16
+ function buildRunSummaryJson(report, publicationRecapSummary) {
17
+ return JSON.stringify({
18
+ latestAssistantMessage: report.assistantMessages.at(-1) ?? null,
19
+ publicationRecapSummary: publicationRecapSummary ?? null,
20
+ });
21
+ }
22
+ function shouldGeneratePublicationRecap(runType) {
23
+ return runType === "implementation"
24
+ || runType === "review_fix"
25
+ || runType === "branch_upkeep"
26
+ || runType === "ci_repair"
27
+ || runType === "queue_repair";
28
+ }
5
29
  export class RunFinalizer {
6
30
  db;
7
31
  logger;
@@ -13,8 +37,9 @@ export class RunFinalizer {
13
37
  failRunAndClear;
14
38
  completionPolicy;
15
39
  completionCheck;
40
+ publicationRecap;
16
41
  feed;
17
- constructor(db, logger, linearSync, enqueueIssue, withHeldLease, releaseLease, appendWakeEventWithLease, failRunAndClear, completionPolicy, completionCheck, feed) {
42
+ constructor(db, logger, linearSync, enqueueIssue, withHeldLease, releaseLease, appendWakeEventWithLease, failRunAndClear, completionPolicy, completionCheck, publicationRecap, feed) {
18
43
  this.db = db;
19
44
  this.logger = logger;
20
45
  this.linearSync = linearSync;
@@ -25,6 +50,7 @@ export class RunFinalizer {
25
50
  this.failRunAndClear = failRunAndClear;
26
51
  this.completionPolicy = completionPolicy;
27
52
  this.completionCheck = completionCheck;
53
+ this.publicationRecap = publicationRecap;
28
54
  this.feed = feed;
29
55
  }
30
56
  buildCompletedRunUpdate(params) {
@@ -32,10 +58,79 @@ export class RunFinalizer {
32
58
  status: "completed",
33
59
  threadId: params.threadId,
34
60
  ...(params.completedTurnId ? { turnId: params.completedTurnId } : {}),
35
- summaryJson: JSON.stringify({ latestAssistantMessage: params.report.assistantMessages.at(-1) ?? null }),
61
+ summaryJson: buildRunSummaryJson(params.report, params.publicationRecapSummary),
36
62
  reportJson: JSON.stringify(params.report),
37
63
  };
38
64
  }
65
+ resolveConsumedWakeEvent(run) {
66
+ return this.db.issueSessions
67
+ .listIssueSessionEvents(run.projectId, run.linearIssueId)
68
+ .filter((event) => event.consumedByRunId === run.id)
69
+ .at(-1);
70
+ }
71
+ resolvePublicationRecapFacts(params) {
72
+ const session = this.db.issueSessions.getIssueSession(params.run.projectId, params.run.linearIssueId);
73
+ const facts = {
74
+ ...(session?.lastWakeReason ? { wakeReason: session.lastWakeReason } : {}),
75
+ ...(params.postRunState ? { postRunState: params.postRunState } : {}),
76
+ ...(params.issue.prNumber !== undefined ? { prNumber: params.issue.prNumber } : {}),
77
+ ...(params.latestAssistantSummary ? { latestAssistantSummary: params.latestAssistantSummary } : {}),
78
+ };
79
+ const wakeEvent = this.resolveConsumedWakeEvent(params.run);
80
+ const payload = parseEventJson(wakeEvent?.eventJson);
81
+ if (!wakeEvent || !payload) {
82
+ return facts;
83
+ }
84
+ switch (wakeEvent.eventType) {
85
+ case "review_changes_requested":
86
+ return {
87
+ ...facts,
88
+ ...(typeof payload.reviewerName === "string" ? { reviewerName: payload.reviewerName } : {}),
89
+ ...(typeof payload.reviewBody === "string" ? { reviewSummary: payload.reviewBody } : {}),
90
+ };
91
+ case "settled_red_ci":
92
+ return {
93
+ ...facts,
94
+ ...(typeof payload.jobName === "string"
95
+ ? { failingCheckName: payload.jobName }
96
+ : typeof payload.checkName === "string" ? { failingCheckName: payload.checkName } : {}),
97
+ ...(typeof payload.summary === "string" ? { failureSummary: payload.summary } : {}),
98
+ };
99
+ case "merge_steward_incident":
100
+ return {
101
+ ...facts,
102
+ ...(typeof payload.incidentSummary === "string" ? { queueIncidentSummary: payload.incidentSummary } : {}),
103
+ };
104
+ default:
105
+ return facts;
106
+ }
107
+ }
108
+ async generatePublicationRecap(params) {
109
+ if (!this.publicationRecap || !shouldGeneratePublicationRecap(params.run.runType)) {
110
+ return undefined;
111
+ }
112
+ try {
113
+ const result = await this.publicationRecap.run({
114
+ issue: params.issue,
115
+ run: params.run,
116
+ facts: this.resolvePublicationRecapFacts({
117
+ run: params.run,
118
+ issue: params.issue,
119
+ postRunState: params.postRunState,
120
+ latestAssistantSummary: params.latestAssistantSummary,
121
+ }),
122
+ });
123
+ return result.summary;
124
+ }
125
+ catch (error) {
126
+ this.logger.warn({
127
+ runId: params.run.id,
128
+ issueKey: params.issue.issueKey,
129
+ error: error instanceof Error ? error.message : String(error),
130
+ }, "Publication recap failed; falling back to the main run summary");
131
+ return undefined;
132
+ }
133
+ }
39
134
  clearProgressAndRelease(run) {
40
135
  this.linearSync.clearProgress(run.id);
41
136
  this.releaseLease(run.projectId, run.linearIssueId);
@@ -142,14 +237,19 @@ export class RunFinalizer {
142
237
  const refreshedIssue = await this.completionPolicy.refreshIssueAfterReactivePublish(run, freshIssue);
143
238
  const postRunFollowUp = await this.completionPolicy.resolvePostRunFollowUp(run, refreshedIssue);
144
239
  const postRunState = postRunFollowUp?.factoryState ?? resolveCompletedRunState(refreshedIssue, run);
240
+ const publicationRecapSummary = await this.generatePublicationRecap({
241
+ run,
242
+ issue: refreshedIssue,
243
+ postRunState,
244
+ latestAssistantSummary: report.assistantMessages.at(-1),
245
+ });
145
246
  const completed = this.withHeldLease(run.projectId, run.linearIssueId, (lease) => {
146
- this.db.runs.finishRun(run.id, {
147
- status: "completed",
247
+ this.db.runs.finishRun(run.id, this.buildCompletedRunUpdate({
148
248
  threadId,
149
- ...(params.completedTurnId ? { turnId: params.completedTurnId } : {}),
150
- summaryJson: JSON.stringify({ latestAssistantMessage: report.assistantMessages.at(-1) ?? null }),
151
- reportJson: JSON.stringify(report),
152
- });
249
+ ...(params.completedTurnId ? { completedTurnId: params.completedTurnId } : {}),
250
+ report,
251
+ publicationRecapSummary,
252
+ }));
153
253
  this.db.issues.upsertIssue({
154
254
  projectId: run.projectId,
155
255
  linearIssueId: run.linearIssueId,
@@ -204,10 +304,12 @@ export class RunFinalizer {
204
304
  summary: params.source === "notification"
205
305
  ? `Turn completed for ${run.runType}`
206
306
  : `Reconciliation: ${run.runType} completed${postRunState ? ` -> ${postRunState}` : ""}`,
207
- detail: report.assistantMessages.at(-1),
307
+ detail: publicationRecapSummary ?? report.assistantMessages.at(-1),
208
308
  });
209
309
  const updatedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? refreshedIssue;
210
- const completionSummary = report.assistantMessages.at(-1)?.slice(0, 300) ?? `${run.runType} completed.`;
310
+ const completionSummary = publicationRecapSummary
311
+ ?? report.assistantMessages.at(-1)?.slice(0, 300)
312
+ ?? `${run.runType} completed.`;
211
313
  const linearActivity = buildRunCompletedActivity({
212
314
  runType: run.runType,
213
315
  completionSummary,
@@ -1,6 +1,7 @@
1
1
  import { summarizeCurrentThread } from "./run-reporting.js";
2
2
  import { buildRunStartedActivity, } from "./linear-session-reporting.js";
3
3
  import { CompletionCheckService } from "./completion-check.js";
4
+ import { PublicationRecapService } from "./publication-recap.js";
4
5
  import { WorktreeManager } from "./worktree-manager.js";
5
6
  import { MergedLinearCompletionReconciler } from "./merged-linear-completion-reconciler.js";
6
7
  import { MainBranchHealthMonitor } from "./main-branch-health-monitor.js";
@@ -59,6 +60,7 @@ export class RunOrchestrator {
59
60
  interruptedRunRecovery;
60
61
  runCompletionPolicy;
61
62
  completionCheck;
63
+ publicationRecap;
62
64
  runNotificationHandler;
63
65
  runReconciler;
64
66
  mergedLinearCompletionReconciler;
@@ -95,7 +97,8 @@ export class RunOrchestrator {
95
97
  this.activeSessionLeases = this.leaseService.activeSessionLeases;
96
98
  this.runCompletionPolicy = new RunCompletionPolicy(config, db, logger, this.leasePorts.withHeldLease);
97
99
  this.completionCheck = new CompletionCheckService(codex, logger);
98
- this.runFinalizer = new RunFinalizer(db, logger, this.linearSync, this.enqueueIssue, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, (lease, issue, runType, context, dedupeScope) => this.appendWakeEventWithLease(lease, issue, runType, context, dedupeScope), this.recoveryPorts.failRunAndClear, this.runCompletionPolicy, this.completionCheck, feed);
100
+ this.publicationRecap = new PublicationRecapService(codex, logger);
101
+ this.runFinalizer = new RunFinalizer(db, logger, this.linearSync, this.enqueueIssue, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, (lease, issue, runType, context, dedupeScope) => this.appendWakeEventWithLease(lease, issue, runType, context, dedupeScope), this.recoveryPorts.failRunAndClear, this.runCompletionPolicy, this.completionCheck, this.publicationRecap, feed);
99
102
  this.runLauncher = new RunLauncher(config, db, codex, logger, this.worktreeManager);
100
103
  this.runNotificationHandler = new RunNotificationHandler(config, db, logger, this.linearSync, this.runFinalizer, this.threadPorts.readThreadWithRetry, this.leasePorts.withHeldLease, this.leasePorts.heartbeatLease, this.leasePorts.releaseLease, feed);
101
104
  this.runRecovery = new RunRecoveryService(db, logger, this.linearSync, this.leasePorts.withHeldLease, this.leasePorts.getHeldLease, (lease, issue, runType, context, dedupeScope) => this.appendWakeEventWithLease(lease, issue, runType, context, dedupeScope), this.leasePorts.releaseLease, (projectId, issueId) => this.enqueueIssue(projectId, issueId), feed);
@@ -36,6 +36,7 @@ export function deriveIssueStatusNote(params) {
36
36
  ? completionCheck.question ?? completionCheck.summary
37
37
  : completionCheck?.summary);
38
38
  const latestRunNote = clean(extractLatestAssistantSummary(params.latestRun));
39
+ const latestFailureReason = clean(params.latestRun?.failureReason);
39
40
  const latestEventNote = clean(eventStatusNote(params.latestEvent));
40
41
  const failureSummary = clean(params.failureSummary);
41
42
  const waitingReason = clean(params.waitingReason);
@@ -50,7 +51,7 @@ export function deriveIssueStatusNote(params) {
50
51
  break;
51
52
  case "failed":
52
53
  case "escalated":
53
- note = latestEventNote ?? completionCheckNote ?? failureSummary ?? latestRunNote ?? sessionSummary;
54
+ note = latestEventNote ?? completionCheckNote ?? failureSummary ?? latestFailureReason ?? latestRunNote ?? sessionSummary;
54
55
  break;
55
56
  case "done":
56
57
  note = completionCheckNote ?? sessionSummary ?? latestRunNote ?? failureSummary;
@@ -2,8 +2,7 @@ export function isInertPatchRelayComment(issue, commentId, body, actorType) {
2
2
  if (commentId === issue.statusCommentId) {
3
3
  return true;
4
4
  }
5
- if (body.startsWith("## PatchRelay status")
6
- && body.includes("_PatchRelay updates this comment as it works. Review and merge remain downstream._")) {
5
+ if (body.startsWith("## PatchRelay status")) {
7
6
  return true;
8
7
  }
9
8
  const normalizedActorType = actorType?.trim().toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.51.1",
3
+ "version": "0.51.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {