patchrelay 0.68.6 → 0.69.0

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,10 +1,7 @@
1
1
  import { buildAgentSessionPlanForIssue, } from "../agent-session-plan.js";
2
2
  import { buildAgentSessionExternalUrls } from "../agent-session-presentation.js";
3
- import { classifyFollowupIntent, followupIntentIsNonActionable } from "../followup-intent.js";
4
- import { extractLatestAssistantSummary } from "../issue-session-events.js";
5
- import { buildAlreadyRunningThought, buildAgentSessionAcknowledgementThought, buildBlockedDelegationActivity, buildDelegationThought, buildFollowupStatusActivity, buildNonActionableFollowupActivity, buildPromptDeliveredThought, buildStopConfirmationActivity, } from "../linear-session-reporting.js";
3
+ import { buildAlreadyRunningThought, buildAgentSessionAcknowledgementThought, buildBlockedDelegationActivity, buildDelegationThought, buildStopConfirmationActivity, } from "../linear-session-reporting.js";
6
4
  import { resolveProject, triggerEventAllowed } from "../project-resolution.js";
7
- import { deriveIssueStatusNote } from "../status-note.js";
8
5
  const PATCHRELAY_AGENT_ACTIVITY_TYPES = new Set([
9
6
  "action",
10
7
  "elicitation",
@@ -20,7 +17,8 @@ export class AgentSessionHandler {
20
17
  wakeDispatcher;
21
18
  logger;
22
19
  feed;
23
- constructor(config, db, linearProvider, codex, wakeDispatcher, logger, feed) {
20
+ conversationAdapter;
21
+ constructor(config, db, linearProvider, codex, wakeDispatcher, logger, feed, conversationAdapter) {
24
22
  this.config = config;
25
23
  this.db = db;
26
24
  this.linearProvider = linearProvider;
@@ -28,6 +26,7 @@ export class AgentSessionHandler {
28
26
  this.wakeDispatcher = wakeDispatcher;
29
27
  this.logger = logger;
30
28
  this.feed = feed;
29
+ this.conversationAdapter = conversationAdapter;
31
30
  }
32
31
  async acknowledgeCreated(normalized) {
33
32
  if (normalized.triggerEvent !== "agentSessionCreated" || !normalized.agentSession?.id || !normalized.issue) {
@@ -65,7 +64,6 @@ export class AgentSessionHandler {
65
64
  return;
66
65
  const existingIssue = this.db.issues.getIssue(project.id, normalized.issue.id);
67
66
  const activeRun = existingIssue?.activeRunId ? this.db.runs.getRunById(existingIssue.activeRunId) : undefined;
68
- const automationEnabled = delegated || existingIssue?.delegatedToPatchRelay === true;
69
67
  if (normalized.triggerEvent === "agentSessionCreated") {
70
68
  if (!delegated) {
71
69
  const latestIssue = this.db.issues.getIssue(project.id, normalized.issue.id);
@@ -128,78 +126,23 @@ export class AgentSessionHandler {
128
126
  }
129
127
  const promptBody = normalized.agentSession.promptBody?.trim();
130
128
  const directReply = promptBody && existingIssue ? params.isDirectReplyToOutstandingQuestion(existingIssue) : false;
131
- const promptIntent = promptBody ? classifyFollowupIntent(promptBody) : undefined;
132
- if (promptBody && existingIssue && promptIntent === "stop") {
133
- await this.handleStopSignal({
134
- normalized,
129
+ if (promptBody && existingIssue && this.conversationAdapter) {
130
+ const result = await this.conversationAdapter.deliverAgentInput({
135
131
  project,
136
- trackedIssue,
137
- activeRun,
138
- linear,
139
- syncAgentSession: (agentSessionId, issue, options) => this.syncAgentSession(linear, agentSessionId, issue, params.peekPendingSessionWakeRunType, options),
140
- });
141
- return;
142
- }
143
- if (promptBody && existingIssue && promptIntent === "status" && !directReply) {
144
- await this.publishAgentActivity(linear, normalized.agentSession.id, this.buildStatusActivity(existingIssue, activeRun, params.peekPendingSessionWakeRunType));
145
- await this.syncAgentSession(linear, normalized.agentSession.id, existingIssue ?? trackedIssue, params.peekPendingSessionWakeRunType, activeRun ? { activeRunType: activeRun.runType } : undefined);
146
- return;
147
- }
148
- if (promptBody && promptIntent && followupIntentIsNonActionable(promptIntent) && !directReply) {
149
- await this.publishAgentActivity(linear, normalized.agentSession.id, buildNonActionableFollowupActivity(promptIntent));
150
- return;
151
- }
152
- if (!automationEnabled && promptBody && existingIssue) {
153
- await this.publishAgentActivity(linear, normalized.agentSession.id, {
154
- type: "thought",
155
- body: "PatchRelay is paused because the issue is undelegated.",
156
- }, { ephemeral: true });
157
- return;
158
- }
159
- if (activeRun && promptBody && activeRun.threadId && activeRun.turnId) {
160
- const input = `New Linear agent prompt received while you are working.\n\n${promptBody}`;
161
- try {
162
- await this.codex.steerTurn({ threadId: activeRun.threadId, turnId: activeRun.turnId, input });
163
- this.feed?.publish({
164
- level: "info",
165
- kind: "agent",
166
- projectId: project.id,
167
- issueKey: trackedIssue?.issueKey,
168
- stage: activeRun.runType,
169
- status: "delivered",
170
- summary: `Delivered follow-up prompt to active ${activeRun.runType} workflow`,
171
- });
172
- }
173
- catch (error) {
174
- this.logger.warn({ issueKey: trackedIssue?.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to deliver follow-up prompt");
175
- this.feed?.publish({
176
- level: "warn",
177
- kind: "agent",
178
- projectId: project.id,
179
- issueKey: trackedIssue?.issueKey,
180
- stage: activeRun.runType,
181
- status: "delivery_failed",
182
- summary: `Could not deliver follow-up prompt to active ${activeRun.runType} workflow`,
183
- });
184
- }
185
- await this.publishAgentActivity(linear, normalized.agentSession.id, buildPromptDeliveredThought(activeRun.runType), { ephemeral: true });
186
- return;
187
- }
188
- if (promptBody && existingIssue && automationEnabled) {
189
- if (!directReply && promptIntent && followupIntentIsNonActionable(promptIntent)) {
190
- await this.publishAgentActivity(linear, normalized.agentSession.id, buildNonActionableFollowupActivity(promptIntent));
191
- return;
192
- }
193
- const queuedRunType = this.wakeDispatcher.recordEventAndDispatch(project.id, normalized.issue.id, {
194
- eventType: directReply ? "direct_reply" : "followup_prompt",
195
- eventJson: JSON.stringify({
196
- text: promptBody,
197
- source: "linear_agent_prompt",
198
- }),
132
+ issue: existingIssue,
133
+ source: "agent_session_prompt",
134
+ body: promptBody,
135
+ directReply,
136
+ emitActivity: (content, options) => this.publishAgentActivity(linear, normalized.agentSession.id, content, options),
137
+ peekPendingSessionWakeRunType: params.peekPendingSessionWakeRunType,
199
138
  });
200
139
  const latestIssue = this.db.issues.getIssue(project.id, normalized.issue.id);
201
- await this.syncAgentSession(linear, normalized.agentSession.id, latestIssue ?? trackedIssue, params.peekPendingSessionWakeRunType, { pendingRunType: queuedRunType ?? wakeRunType ?? (existingIssue.prReviewState === "changes_requested" ? "review_fix" : "implementation") });
202
- await this.publishAgentActivity(linear, normalized.agentSession.id, buildPromptDeliveredThought(queuedRunType ?? wakeRunType ?? "implementation"), { ephemeral: true });
140
+ const syncOptions = result.activeRunType
141
+ ? { activeRunType: result.activeRunType }
142
+ : result.queuedRunType ? { pendingRunType: result.queuedRunType }
143
+ : wakeRunType ? { pendingRunType: wakeRunType }
144
+ : undefined;
145
+ await this.syncAgentSession(linear, normalized.agentSession.id, latestIssue ?? trackedIssue, params.peekPendingSessionWakeRunType, syncOptions);
203
146
  return;
204
147
  }
205
148
  if (wakeRunType) {
@@ -208,24 +151,6 @@ export class AgentSessionHandler {
208
151
  await this.publishAgentActivity(linear, normalized.agentSession.id, buildDelegationThought(wakeRunType, "prompt"), { ephemeral: true });
209
152
  }
210
153
  }
211
- buildStatusActivity(issue, activeRun, peekPendingSessionWakeRunType) {
212
- const latestRun = activeRun ?? this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
213
- const latestEvent = this.db.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId).at(-1);
214
- const statusNote = deriveIssueStatusNote({
215
- issue,
216
- latestRun,
217
- latestEvent,
218
- sessionSummary: extractLatestAssistantSummary(latestRun),
219
- waitingReason: undefined,
220
- });
221
- const pendingRunType = peekPendingSessionWakeRunType(issue.projectId, issue.linearIssueId);
222
- return buildFollowupStatusActivity({
223
- issue,
224
- ...(statusNote ? { statusNote } : {}),
225
- ...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}),
226
- ...(pendingRunType ? { pendingRunType } : {}),
227
- });
228
- }
229
154
  async handleStopSignal(params) {
230
155
  const issueId = params.normalized.issue.id;
231
156
  const sessionId = params.normalized.agentSession.id;
@@ -31,6 +31,7 @@ export function isPatchRelayGeneratedActivityComment(body) {
31
31
  || body.startsWith("PatchRelay is already working on ")
32
32
  || body.startsWith("PatchRelay received the ")
33
33
  || body.startsWith("PatchRelay routed your latest instructions into ")
34
+ || body.startsWith("PatchRelay could not route your latest instructions into ")
34
35
  || body.startsWith("PatchRelay status:")
35
36
  || body.startsWith("PatchRelay did not start implementation ")
36
37
  || body.startsWith("PatchRelay has stopped work as requested.")
@@ -38,5 +39,22 @@ export function isPatchRelayGeneratedActivityComment(body) {
38
39
  || body === "This thread is for an agent session with patchrelay.";
39
40
  }
40
41
  export function hasExplicitPatchRelayWakeIntent(body) {
41
- return /\bpatchrelay\b/i.test(body);
42
+ return extractPatchRelayAddressedText(body) !== undefined;
43
+ }
44
+ export function extractPatchRelayAddressedText(body) {
45
+ const trimmed = body.trim();
46
+ if (!trimmed)
47
+ return undefined;
48
+ const patterns = [
49
+ /^@?patchrelay\b[\s,:;-]*/i,
50
+ /^\bhey\s+@?patchrelay\b[\s,:;-]*/i,
51
+ ];
52
+ for (const pattern of patterns) {
53
+ const match = trimmed.match(pattern);
54
+ if (!match)
55
+ continue;
56
+ const rest = trimmed.slice(match[0].length).trim();
57
+ return rest || trimmed;
58
+ }
59
+ return undefined;
42
60
  }
@@ -1,20 +1,17 @@
1
- import { classifyFollowupIntent, followupIntentIsNonActionable, followupIntentQueuesWork } from "../followup-intent.js";
2
1
  import { triggerEventAllowed } from "../project-resolution.js";
3
- import { hasExplicitPatchRelayWakeIntent, isInertPatchRelayComment, isPatchRelayManagedCommentAuthor, } from "./comment-policy.js";
4
- import { classifyIssue } from "../issue-class.js";
5
- const ENQUEUEABLE_STATES = new Set(["pr_open", "changes_requested", "implementing", "delegated", "awaiting_input"]);
2
+ import { extractPatchRelayAddressedText, isInertPatchRelayComment, isPatchRelayManagedCommentAuthor, } from "./comment-policy.js";
6
3
  export class CommentWakeHandler {
7
4
  db;
8
- codex;
9
5
  wakeDispatcher;
10
- logger;
11
6
  feed;
12
- constructor(db, codex, wakeDispatcher, logger, feed) {
7
+ conversationAdapter;
8
+ emitLinearActivity;
9
+ constructor(db, wakeDispatcher, feed, conversationAdapter, emitLinearActivity) {
13
10
  this.db = db;
14
- this.codex = codex;
15
11
  this.wakeDispatcher = wakeDispatcher;
16
- this.logger = logger;
17
12
  this.feed = feed;
13
+ this.conversationAdapter = conversationAdapter;
14
+ this.emitLinearActivity = emitLinearActivity;
18
15
  }
19
16
  async handle(params) {
20
17
  const { normalized, project, trackedIssue } = params;
@@ -28,10 +25,6 @@ export class CommentWakeHandler {
28
25
  const issue = this.db.issues.getIssue(project.id, normalized.issue.id);
29
26
  if (!issue)
30
27
  return;
31
- const issueClass = classifyIssue({
32
- issue,
33
- childIssueCount: this.db.issues.listCanonicalChildIssues(project.id, normalized.issue.id).length,
34
- }).issueClass;
35
28
  const trimmedBody = normalized.comment.body.trim();
36
29
  const installation = this.db.linearInstallations.getLinearInstallationForProject(project.id);
37
30
  const selfAuthored = isPatchRelayManagedCommentAuthor(installation, normalized.actor, normalized.comment.userName);
@@ -58,168 +51,39 @@ export class CommentWakeHandler {
58
51
  });
59
52
  return;
60
53
  }
61
- const directReply = params.isDirectReplyToOutstandingQuestion(issue);
62
- const intent = classifyFollowupIntent(trimmedBody);
63
- if (!issue.activeRunId) {
64
- if (ENQUEUEABLE_STATES.has(issue.factoryState)) {
65
- const wakeIntent = issueClass === "orchestration" || directReply || hasExplicitPatchRelayWakeIntent(trimmedBody);
66
- if (!wakeIntent) {
67
- this.feed?.publish({
68
- level: "info",
69
- kind: "comment",
70
- projectId: project.id,
71
- issueKey: trackedIssue?.issueKey,
72
- status: "ignored",
73
- summary: "Ignored comment with no explicit PatchRelay wake intent",
74
- detail: trimmedBody.slice(0, 200),
75
- });
76
- return;
77
- }
78
- if (intent === "stop") {
79
- this.wakeDispatcher.recordEventAndDispatch(project.id, normalized.issue.id, {
80
- eventType: "stop_requested",
81
- eventJson: JSON.stringify({
82
- body: trimmedBody,
83
- author: normalized.comment.userName,
84
- }),
85
- });
86
- this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(project.id, normalized.issue.id);
87
- this.feed?.publish({
88
- level: "info",
89
- kind: "comment",
90
- projectId: project.id,
91
- issueKey: trackedIssue?.issueKey,
92
- status: "stopped",
93
- summary: "Stop request recorded from Linear comment",
94
- detail: trimmedBody.slice(0, 200),
95
- });
96
- return;
97
- }
98
- if (!directReply && !followupIntentQueuesWork(intent)) {
99
- this.feed?.publish({
100
- level: "info",
101
- kind: "comment",
102
- projectId: project.id,
103
- issueKey: trackedIssue?.issueKey,
104
- status: intent === "status" ? "status_requested" : "ignored",
105
- summary: intent === "status"
106
- ? "Ignored status comment without queueing work"
107
- : "Ignored non-actionable follow-up comment",
108
- detail: trimmedBody.slice(0, 200),
109
- });
110
- return;
111
- }
112
- const runType = issue.prReviewState === "changes_requested" ? "review_fix" : "implementation";
113
- const queuedRunType = this.wakeDispatcher.recordEventAndDispatch(project.id, normalized.issue.id, {
114
- eventType: directReply ? "direct_reply" : "followup_comment",
115
- eventJson: JSON.stringify({
116
- body: trimmedBody,
117
- author: normalized.comment.userName,
118
- }),
119
- });
120
- this.feed?.publish({
121
- level: "info",
122
- kind: "comment",
123
- projectId: project.id,
124
- issueKey: trackedIssue?.issueKey,
125
- status: "enqueued",
126
- summary: `Comment enqueued ${(queuedRunType ?? runType)} run`,
127
- detail: trimmedBody.slice(0, 200),
128
- });
129
- }
130
- return;
131
- }
132
- const run = this.db.runs.getRunById(issue.activeRunId);
133
- if (!run?.threadId || !run.turnId)
134
- return;
135
- if (intent === "stop") {
136
- try {
137
- await this.codex.steerTurn({
138
- threadId: run.threadId,
139
- turnId: run.turnId,
140
- input: "STOP: The user has requested you stop working immediately. Do not make further changes. Wrap up and exit.",
141
- });
142
- }
143
- catch (error) {
144
- this.logger.warn({ issueKey: trackedIssue?.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to steer Codex turn for comment stop request");
145
- }
146
- this.db.runs.finishRun(run.id, { status: "released", threadId: run.threadId, turnId: run.turnId });
147
- this.db.issueSessions.upsertIssueRespectingActiveLease(project.id, normalized.issue.id, {
148
- projectId: project.id,
149
- linearIssueId: normalized.issue.id,
150
- activeRunId: null,
151
- factoryState: "awaiting_input",
152
- });
153
- this.wakeDispatcher.recordEventAndDispatch(project.id, normalized.issue.id, {
154
- eventType: "stop_requested",
155
- eventJson: JSON.stringify({
156
- body: trimmedBody,
157
- author: normalized.comment.userName,
158
- }),
159
- });
160
- this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(project.id, normalized.issue.id);
54
+ const addressedText = extractPatchRelayAddressedText(trimmedBody);
55
+ if (!addressedText) {
161
56
  this.feed?.publish({
162
57
  level: "info",
163
58
  kind: "comment",
164
59
  projectId: project.id,
165
60
  issueKey: trackedIssue?.issueKey,
166
- stage: run.runType,
167
- status: "stopped",
168
- summary: "Stop request delivered to active workflow",
169
- });
170
- return;
171
- }
172
- if (!directReply && followupIntentIsNonActionable(intent)) {
173
- this.feed?.publish({
174
- level: "info",
175
- kind: "comment",
176
- projectId: project.id,
177
- issueKey: trackedIssue?.issueKey,
178
- stage: run.runType,
179
- status: intent === "status" ? "status_requested" : "ignored",
180
- summary: intent === "status"
181
- ? "Ignored status comment without steering active workflow"
182
- : "Ignored non-actionable follow-up comment",
61
+ status: "ignored",
62
+ summary: "Ignored issue comment because it did not address PatchRelay",
183
63
  detail: trimmedBody.slice(0, 200),
184
64
  });
185
65
  return;
186
66
  }
187
- const body = [
188
- "New Linear comment received while you are working.",
189
- normalized.comment.userName ? `Author: ${normalized.comment.userName}` : undefined,
190
- "",
191
- trimmedBody,
192
- ].filter(Boolean).join("\n");
193
- try {
194
- await this.codex.steerTurn({ threadId: run.threadId, turnId: run.turnId, input: body });
67
+ const result = await this.conversationAdapter?.deliverAgentInput({
68
+ project,
69
+ issue,
70
+ source: "addressed_issue_comment",
71
+ body: addressedText,
72
+ author: normalized.comment.userName,
73
+ directReply: params.isDirectReplyToOutstandingQuestion(issue),
74
+ emitActivity: this.emitLinearActivity
75
+ ? (content, options) => this.emitLinearActivity(issue, content, options)
76
+ : undefined,
77
+ });
78
+ if (result?.queuedRunType) {
195
79
  this.feed?.publish({
196
80
  level: "info",
197
81
  kind: "comment",
198
82
  projectId: project.id,
199
83
  issueKey: trackedIssue?.issueKey,
200
- stage: run.runType,
201
- status: "delivered",
202
- summary: `Delivered follow-up comment to active ${run.runType} workflow`,
203
- });
204
- }
205
- catch (error) {
206
- this.logger.warn({ issueKey: trackedIssue?.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to deliver follow-up comment");
207
- const directReply = params.isDirectReplyToOutstandingQuestion(issue);
208
- this.wakeDispatcher.recordEventAndDispatch(project.id, normalized.issue.id, {
209
- eventType: directReply ? "direct_reply" : "followup_comment",
210
- eventJson: JSON.stringify({
211
- body: trimmedBody,
212
- author: normalized.comment.userName,
213
- }),
214
- });
215
- this.feed?.publish({
216
- level: "warn",
217
- kind: "comment",
218
- projectId: project.id,
219
- issueKey: trackedIssue?.issueKey,
220
- stage: run.runType,
221
- status: "delivery_failed",
222
- summary: `Could not deliver follow-up comment to active ${run.runType} workflow`,
84
+ status: "enqueued",
85
+ summary: `Comment enqueued ${result.queuedRunType} run`,
86
+ detail: addressedText.slice(0, 200),
223
87
  });
224
88
  }
225
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.68.6",
3
+ "version": "0.69.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {