patchrelay 0.69.2 → 0.69.4
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.
- package/dist/agent-input-service.js +300 -0
- package/dist/build-info.json +3 -3
- package/dist/codex-conversation-adapter.js +1 -270
- package/dist/db/issue-session-store.js +2 -13
- package/dist/db.js +5 -81
- package/dist/idle-reconciliation.js +1 -1
- package/dist/interrupted-run-recovery.js +1 -1
- package/dist/issue-overview-query.js +1 -1
- package/dist/run-wake-planner.js +1 -1
- package/dist/service-issue-actions.js +20 -32
- package/dist/service-startup-recovery.js +2 -2
- package/dist/service.js +5 -2
- package/dist/tracked-issue-list-query.js +1 -1
- package/dist/tracked-issue-query.js +5 -3
- package/dist/wake-dispatcher.js +2 -2
- package/dist/webhook-handler.js +6 -6
- package/dist/webhooks/agent-session-handler.js +6 -6
- package/dist/webhooks/comment-wake-handler.js +5 -5
- package/dist/webhooks/desired-stage-recorder.js +1 -1
- package/dist/workflow-wake-resolver.js +108 -0
- package/package.json +1 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { buildFollowupStatusActivity, buildNonActionableFollowupActivity, buildPromptDeliveryFailedActivity, buildPromptDeliveredThought, } from "./linear-session-reporting.js";
|
|
2
|
+
import { deriveIssueStatusNote } from "./status-note.js";
|
|
3
|
+
import { extractLatestAssistantSummary } from "./issue-session-events.js";
|
|
4
|
+
export class AgentInputService {
|
|
5
|
+
db;
|
|
6
|
+
codex;
|
|
7
|
+
wakeDispatcher;
|
|
8
|
+
logger;
|
|
9
|
+
feed;
|
|
10
|
+
followupClassifier;
|
|
11
|
+
constructor(db, codex, wakeDispatcher, logger, feed, followupClassifier) {
|
|
12
|
+
this.db = db;
|
|
13
|
+
this.codex = codex;
|
|
14
|
+
this.wakeDispatcher = wakeDispatcher;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
this.feed = feed;
|
|
17
|
+
this.followupClassifier = followupClassifier;
|
|
18
|
+
}
|
|
19
|
+
async deliverAgentInput(params) {
|
|
20
|
+
const body = params.body.trim();
|
|
21
|
+
if (!body)
|
|
22
|
+
return { status: "ignored" };
|
|
23
|
+
const issue = this.db.issues.getIssue(params.issue.projectId, params.issue.linearIssueId) ?? params.issue;
|
|
24
|
+
const activeRun = issue.activeRunId ? this.db.runs.getRunById(issue.activeRunId) : undefined;
|
|
25
|
+
const intent = await this.classify(body, params.source, issue, activeRun, params.directReply === true);
|
|
26
|
+
if (intent?.intent === "status" && !params.directReply) {
|
|
27
|
+
await params.emitActivity?.(this.buildStatusActivity(issue, activeRun, params.peekPendingSessionWakeRunType, activeRun ? "thought" : "response"), activeRun ? { ephemeral: true } : undefined);
|
|
28
|
+
return { status: "answered", ...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}) };
|
|
29
|
+
}
|
|
30
|
+
if (intent?.intent === "stop") {
|
|
31
|
+
if (activeRun) {
|
|
32
|
+
await this.stopActiveRun(issue, activeRun, body, params.source);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
|
|
36
|
+
eventType: "stop_requested",
|
|
37
|
+
eventJson: JSON.stringify({ body, source: params.source, ...(params.author ? { author: params.author } : {}) }),
|
|
38
|
+
});
|
|
39
|
+
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
40
|
+
}
|
|
41
|
+
return { status: "stopped", ...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}) };
|
|
42
|
+
}
|
|
43
|
+
if (!issue.delegatedToPatchRelay && !(activeRun && params.source === "patchrelay_operator_prompt")) {
|
|
44
|
+
await params.emitActivity?.({ type: "thought", body: "PatchRelay is paused because the issue is undelegated." }, { ephemeral: true });
|
|
45
|
+
return { status: "ignored" };
|
|
46
|
+
}
|
|
47
|
+
if (activeRun) {
|
|
48
|
+
return await this.steerActiveRun({
|
|
49
|
+
issue,
|
|
50
|
+
activeRun,
|
|
51
|
+
body,
|
|
52
|
+
source: params.source,
|
|
53
|
+
author: params.author,
|
|
54
|
+
operatorSource: params.operatorSource,
|
|
55
|
+
emitActivity: params.emitActivity,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return await this.queueIdleInput({
|
|
59
|
+
project: params.project,
|
|
60
|
+
issue,
|
|
61
|
+
body,
|
|
62
|
+
source: params.source,
|
|
63
|
+
author: params.author,
|
|
64
|
+
operatorSource: params.operatorSource,
|
|
65
|
+
directReply: params.directReply === true,
|
|
66
|
+
emitActivity: params.emitActivity,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async classify(body, source, issue, activeRun, directReply) {
|
|
70
|
+
if (source === "patchrelay_operator_prompt")
|
|
71
|
+
return undefined;
|
|
72
|
+
if (!this.followupClassifier)
|
|
73
|
+
return undefined;
|
|
74
|
+
return await this.followupClassifier.classify(body, {
|
|
75
|
+
source: source === "linear_agent_session" ? "agentPrompted" : "comment",
|
|
76
|
+
...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}),
|
|
77
|
+
factoryState: issue.factoryState,
|
|
78
|
+
directReply,
|
|
79
|
+
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
80
|
+
prReviewState: issue.prReviewState,
|
|
81
|
+
explicitWakeIntent: true,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async steerActiveRun(params) {
|
|
85
|
+
const { issue, activeRun, body, source } = params;
|
|
86
|
+
if (!activeRun.threadId || !activeRun.turnId) {
|
|
87
|
+
const queuedRunType = this.queueFollowUpEvent(issue, body, source, params.author, params.operatorSource, false);
|
|
88
|
+
return { status: "queued", ...(queuedRunType ? { queuedRunType } : {}) };
|
|
89
|
+
}
|
|
90
|
+
const input = [
|
|
91
|
+
source === "linear_agent_session"
|
|
92
|
+
? "New Linear agent-session prompt received while you are working."
|
|
93
|
+
: source === "linear_addressed_comment"
|
|
94
|
+
? "New explicitly addressed Linear issue comment received while you are working."
|
|
95
|
+
: "New PatchRelay operator prompt received while you are working.",
|
|
96
|
+
params.author ? `Author: ${params.author}` : undefined,
|
|
97
|
+
"",
|
|
98
|
+
"Checkpoint contract: incorporate this instruction before your next meaningful side effect when possible. If you are already inside a non-interruptible command, finish that command, then re-plan with this input before continuing.",
|
|
99
|
+
"",
|
|
100
|
+
body,
|
|
101
|
+
].filter(Boolean).join("\n");
|
|
102
|
+
try {
|
|
103
|
+
await this.codex.steerTurn({ threadId: activeRun.threadId, turnId: activeRun.turnId, input });
|
|
104
|
+
this.recordPromptDelivery({
|
|
105
|
+
issue,
|
|
106
|
+
source,
|
|
107
|
+
runId: activeRun.id,
|
|
108
|
+
runType: activeRun.runType,
|
|
109
|
+
status: "delivered",
|
|
110
|
+
body,
|
|
111
|
+
primitive: "turn/steer",
|
|
112
|
+
threadId: activeRun.threadId,
|
|
113
|
+
turnId: activeRun.turnId,
|
|
114
|
+
});
|
|
115
|
+
this.feed?.publish({
|
|
116
|
+
level: "info",
|
|
117
|
+
kind: inputFeedKind(source),
|
|
118
|
+
projectId: issue.projectId,
|
|
119
|
+
issueKey: issue.issueKey,
|
|
120
|
+
stage: activeRun.runType,
|
|
121
|
+
status: "delivered",
|
|
122
|
+
summary: `Delivered agent input to active ${activeRun.runType} workflow`,
|
|
123
|
+
});
|
|
124
|
+
await params.emitActivity?.(buildPromptDeliveredThought(activeRun.runType), { ephemeral: true });
|
|
125
|
+
return { status: "steered", activeRunType: activeRun.runType };
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
129
|
+
this.logger.warn({ issueKey: issue.issueKey, error: message }, "Failed to deliver agent input to active Codex turn");
|
|
130
|
+
this.recordPromptDelivery({
|
|
131
|
+
issue,
|
|
132
|
+
source,
|
|
133
|
+
runId: activeRun.id,
|
|
134
|
+
runType: activeRun.runType,
|
|
135
|
+
status: "delivery_failed",
|
|
136
|
+
body,
|
|
137
|
+
primitive: "turn/steer",
|
|
138
|
+
threadId: activeRun.threadId,
|
|
139
|
+
turnId: activeRun.turnId,
|
|
140
|
+
error: message,
|
|
141
|
+
});
|
|
142
|
+
const queuedRunType = this.queueFollowUpEvent(issue, body, source, params.author, params.operatorSource, false);
|
|
143
|
+
this.feed?.publish({
|
|
144
|
+
level: "warn",
|
|
145
|
+
kind: inputFeedKind(source),
|
|
146
|
+
projectId: issue.projectId,
|
|
147
|
+
issueKey: issue.issueKey,
|
|
148
|
+
stage: activeRun.runType,
|
|
149
|
+
status: "delivery_failed",
|
|
150
|
+
summary: `Could not deliver agent input to active ${activeRun.runType} workflow`,
|
|
151
|
+
});
|
|
152
|
+
await params.emitActivity?.(buildPromptDeliveryFailedActivity(activeRun.runType, message));
|
|
153
|
+
return { status: "delivery_failed", activeRunType: activeRun.runType, ...(queuedRunType ? { queuedRunType } : {}) };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async queueIdleInput(params) {
|
|
157
|
+
const originalIssue = params.issue;
|
|
158
|
+
let issue = originalIssue;
|
|
159
|
+
const replacementPrRequired = originalIssue.factoryState === "done" && originalIssue.prNumber !== undefined;
|
|
160
|
+
if (replacementPrRequired) {
|
|
161
|
+
issue = this.prepareReplacementWork(params.project, originalIssue);
|
|
162
|
+
}
|
|
163
|
+
const queuedRunType = this.queueFollowUpEvent(issue, params.body, params.source, params.author, params.operatorSource, params.directReply, replacementPrRequired ? originalIssue : undefined);
|
|
164
|
+
if (queuedRunType) {
|
|
165
|
+
await params.emitActivity?.(replacementPrRequired
|
|
166
|
+
? { type: "action", action: "Reopening", parameter: `completed PR #${originalIssue.prNumber} for replacement work` }
|
|
167
|
+
: buildPromptDeliveredThought(queuedRunType), { ephemeral: true });
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
await params.emitActivity?.(buildNonActionableFollowupActivity("unknown_needs_ack"));
|
|
171
|
+
}
|
|
172
|
+
return { status: queuedRunType ? "queued" : "ignored", ...(queuedRunType ? { queuedRunType } : {}) };
|
|
173
|
+
}
|
|
174
|
+
queueFollowUpEvent(issue, body, source, author, operatorSource, directReply, previousIssue) {
|
|
175
|
+
return this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
|
|
176
|
+
eventType: directReply ? "direct_reply" : inputSourceEventType(source),
|
|
177
|
+
eventJson: JSON.stringify({
|
|
178
|
+
...(source === "linear_addressed_comment" ? { body } : { text: body }),
|
|
179
|
+
source: inputSourcePayloadSource(source),
|
|
180
|
+
...(author ? { author } : {}),
|
|
181
|
+
...(operatorSource ? { operatorSource } : {}),
|
|
182
|
+
...(previousIssue?.prNumber !== undefined
|
|
183
|
+
? {
|
|
184
|
+
replacementPrRequired: true,
|
|
185
|
+
previousPrNumber: previousIssue.prNumber,
|
|
186
|
+
...(previousIssue.prUrl ? { previousPrUrl: previousIssue.prUrl } : {}),
|
|
187
|
+
...(previousIssue.prState ? { previousPrState: previousIssue.prState } : {}),
|
|
188
|
+
...(previousIssue.prHeadSha ? { previousPrHeadSha: previousIssue.prHeadSha } : {}),
|
|
189
|
+
}
|
|
190
|
+
: {}),
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
prepareReplacementWork(project, issue) {
|
|
195
|
+
const issueRef = (issue.issueKey ?? issue.linearIssueId).replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
196
|
+
const suffix = Date.now().toString(36);
|
|
197
|
+
return this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
198
|
+
projectId: issue.projectId,
|
|
199
|
+
linearIssueId: issue.linearIssueId,
|
|
200
|
+
factoryState: "delegated",
|
|
201
|
+
branchName: `${project.branchPrefix}/${issueRef}-replacement-${suffix}`,
|
|
202
|
+
prNumber: null,
|
|
203
|
+
prUrl: null,
|
|
204
|
+
prState: null,
|
|
205
|
+
prIsDraft: null,
|
|
206
|
+
prHeadSha: null,
|
|
207
|
+
prAuthorLogin: null,
|
|
208
|
+
prReviewState: null,
|
|
209
|
+
prCheckStatus: null,
|
|
210
|
+
lastBlockingReviewHeadSha: null,
|
|
211
|
+
}) ?? issue;
|
|
212
|
+
}
|
|
213
|
+
async stopActiveRun(issue, run, body, source) {
|
|
214
|
+
if (run.threadId && run.turnId) {
|
|
215
|
+
try {
|
|
216
|
+
await this.codex.steerTurn({
|
|
217
|
+
threadId: run.threadId,
|
|
218
|
+
turnId: run.turnId,
|
|
219
|
+
input: "STOP: The user has requested you stop working immediately. Do not make further changes. Wrap up and exit.",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
this.logger.warn({ issueKey: issue.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to steer Codex turn for stop request");
|
|
224
|
+
}
|
|
225
|
+
this.db.runs.finishRun(run.id, { status: "released", threadId: run.threadId, turnId: run.turnId });
|
|
226
|
+
}
|
|
227
|
+
this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
228
|
+
projectId: issue.projectId,
|
|
229
|
+
linearIssueId: issue.linearIssueId,
|
|
230
|
+
activeRunId: null,
|
|
231
|
+
factoryState: "awaiting_input",
|
|
232
|
+
});
|
|
233
|
+
this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
|
|
234
|
+
eventType: "stop_requested",
|
|
235
|
+
eventJson: JSON.stringify({ body, source }),
|
|
236
|
+
});
|
|
237
|
+
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
238
|
+
this.db.issueSessions.releaseIssueSessionLeaseRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
239
|
+
}
|
|
240
|
+
buildStatusActivity(issue, activeRun, peekPendingSessionWakeRunType, activityType) {
|
|
241
|
+
const latestRun = activeRun ?? this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
242
|
+
const latestEvent = this.db.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId).at(-1);
|
|
243
|
+
const statusNote = deriveIssueStatusNote({
|
|
244
|
+
issue,
|
|
245
|
+
latestRun,
|
|
246
|
+
latestEvent,
|
|
247
|
+
sessionSummary: extractLatestAssistantSummary(latestRun),
|
|
248
|
+
waitingReason: undefined,
|
|
249
|
+
});
|
|
250
|
+
const pendingRunType = peekPendingSessionWakeRunType?.(issue.projectId, issue.linearIssueId);
|
|
251
|
+
return buildFollowupStatusActivity({
|
|
252
|
+
issue,
|
|
253
|
+
...(statusNote ? { statusNote } : {}),
|
|
254
|
+
...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}),
|
|
255
|
+
...(pendingRunType ? { pendingRunType } : {}),
|
|
256
|
+
activityType,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
recordPromptDelivery(params) {
|
|
260
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.issue.projectId, params.issue.linearIssueId, {
|
|
261
|
+
projectId: params.issue.projectId,
|
|
262
|
+
linearIssueId: params.issue.linearIssueId,
|
|
263
|
+
eventType: "prompt_delivered",
|
|
264
|
+
eventJson: JSON.stringify({
|
|
265
|
+
source: inputSourcePayloadSource(params.source),
|
|
266
|
+
runId: params.runId,
|
|
267
|
+
runType: params.runType,
|
|
268
|
+
status: params.status,
|
|
269
|
+
body: params.body,
|
|
270
|
+
primitive: params.primitive,
|
|
271
|
+
...(params.threadId ? { threadId: params.threadId } : {}),
|
|
272
|
+
...(params.turnId ? { turnId: params.turnId } : {}),
|
|
273
|
+
...(params.error ? { error: params.error } : {}),
|
|
274
|
+
}),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function inputSourceEventType(source) {
|
|
279
|
+
switch (source) {
|
|
280
|
+
case "linear_agent_session":
|
|
281
|
+
return "followup_prompt";
|
|
282
|
+
case "linear_addressed_comment":
|
|
283
|
+
return "followup_comment";
|
|
284
|
+
case "patchrelay_operator_prompt":
|
|
285
|
+
return "operator_prompt";
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function inputSourcePayloadSource(source) {
|
|
289
|
+
switch (source) {
|
|
290
|
+
case "linear_agent_session":
|
|
291
|
+
return "linear_agent_prompt";
|
|
292
|
+
case "linear_addressed_comment":
|
|
293
|
+
return "linear_comment";
|
|
294
|
+
case "patchrelay_operator_prompt":
|
|
295
|
+
return "patchrelay_operator_prompt";
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function inputFeedKind(source) {
|
|
299
|
+
return source === "linear_agent_session" ? "agent" : "comment";
|
|
300
|
+
}
|
package/dist/build-info.json
CHANGED
|
@@ -1,270 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { deriveIssueStatusNote } from "./status-note.js";
|
|
3
|
-
import { extractLatestAssistantSummary } from "./issue-session-events.js";
|
|
4
|
-
export class CodexConversationAdapter {
|
|
5
|
-
db;
|
|
6
|
-
codex;
|
|
7
|
-
wakeDispatcher;
|
|
8
|
-
logger;
|
|
9
|
-
feed;
|
|
10
|
-
followupClassifier;
|
|
11
|
-
constructor(db, codex, wakeDispatcher, logger, feed, followupClassifier) {
|
|
12
|
-
this.db = db;
|
|
13
|
-
this.codex = codex;
|
|
14
|
-
this.wakeDispatcher = wakeDispatcher;
|
|
15
|
-
this.logger = logger;
|
|
16
|
-
this.feed = feed;
|
|
17
|
-
this.followupClassifier = followupClassifier;
|
|
18
|
-
}
|
|
19
|
-
async deliverAgentInput(params) {
|
|
20
|
-
const body = params.body.trim();
|
|
21
|
-
if (!body)
|
|
22
|
-
return { status: "ignored" };
|
|
23
|
-
const issue = this.db.issues.getIssue(params.issue.projectId, params.issue.linearIssueId) ?? params.issue;
|
|
24
|
-
const activeRun = issue.activeRunId ? this.db.runs.getRunById(issue.activeRunId) : undefined;
|
|
25
|
-
const intent = await this.classify(body, params.source, issue, activeRun, params.directReply === true);
|
|
26
|
-
if (intent?.intent === "status" && !params.directReply) {
|
|
27
|
-
await params.emitActivity?.(this.buildStatusActivity(issue, activeRun, params.peekPendingSessionWakeRunType, activeRun ? "thought" : "response"), activeRun ? { ephemeral: true } : undefined);
|
|
28
|
-
return { status: "answered", ...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}) };
|
|
29
|
-
}
|
|
30
|
-
if (intent?.intent === "stop") {
|
|
31
|
-
if (activeRun) {
|
|
32
|
-
await this.stopActiveRun(issue, activeRun, body, params.source);
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
|
|
36
|
-
eventType: "stop_requested",
|
|
37
|
-
eventJson: JSON.stringify({ body, source: params.source, ...(params.author ? { author: params.author } : {}) }),
|
|
38
|
-
});
|
|
39
|
-
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
40
|
-
}
|
|
41
|
-
return { status: "stopped", ...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}) };
|
|
42
|
-
}
|
|
43
|
-
if (!issue.delegatedToPatchRelay) {
|
|
44
|
-
await params.emitActivity?.({ type: "thought", body: "PatchRelay is paused because the issue is undelegated." }, { ephemeral: true });
|
|
45
|
-
return { status: "ignored" };
|
|
46
|
-
}
|
|
47
|
-
if (activeRun) {
|
|
48
|
-
return await this.steerActiveRun({
|
|
49
|
-
issue,
|
|
50
|
-
activeRun,
|
|
51
|
-
body,
|
|
52
|
-
source: params.source,
|
|
53
|
-
author: params.author,
|
|
54
|
-
emitActivity: params.emitActivity,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return await this.queueIdleInput({
|
|
58
|
-
project: params.project,
|
|
59
|
-
issue,
|
|
60
|
-
body,
|
|
61
|
-
source: params.source,
|
|
62
|
-
author: params.author,
|
|
63
|
-
directReply: params.directReply === true,
|
|
64
|
-
emitActivity: params.emitActivity,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
async classify(body, source, issue, activeRun, directReply) {
|
|
68
|
-
if (!this.followupClassifier)
|
|
69
|
-
return undefined;
|
|
70
|
-
return await this.followupClassifier.classify(body, {
|
|
71
|
-
source: source === "agent_session_prompt" ? "agentPrompted" : "comment",
|
|
72
|
-
...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}),
|
|
73
|
-
factoryState: issue.factoryState,
|
|
74
|
-
directReply,
|
|
75
|
-
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
76
|
-
prReviewState: issue.prReviewState,
|
|
77
|
-
explicitWakeIntent: true,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
async steerActiveRun(params) {
|
|
81
|
-
const { issue, activeRun, body, source } = params;
|
|
82
|
-
if (!activeRun.threadId || !activeRun.turnId) {
|
|
83
|
-
const queuedRunType = this.queueFollowUpEvent(issue, body, source, params.author, false);
|
|
84
|
-
return { status: "queued", ...(queuedRunType ? { queuedRunType } : {}) };
|
|
85
|
-
}
|
|
86
|
-
const input = [
|
|
87
|
-
source === "agent_session_prompt"
|
|
88
|
-
? "New Linear agent-session prompt received while you are working."
|
|
89
|
-
: "New explicitly addressed Linear issue comment received while you are working.",
|
|
90
|
-
params.author ? `Author: ${params.author}` : undefined,
|
|
91
|
-
"",
|
|
92
|
-
"Checkpoint contract: incorporate this instruction before your next meaningful side effect when possible. If you are already inside a non-interruptible command, finish that command, then re-plan with this input before continuing.",
|
|
93
|
-
"",
|
|
94
|
-
body,
|
|
95
|
-
].filter(Boolean).join("\n");
|
|
96
|
-
try {
|
|
97
|
-
await this.codex.steerTurn({ threadId: activeRun.threadId, turnId: activeRun.turnId, input });
|
|
98
|
-
this.recordPromptDelivery({
|
|
99
|
-
issue,
|
|
100
|
-
source,
|
|
101
|
-
runId: activeRun.id,
|
|
102
|
-
runType: activeRun.runType,
|
|
103
|
-
status: "delivered",
|
|
104
|
-
body,
|
|
105
|
-
primitive: "turn/steer",
|
|
106
|
-
threadId: activeRun.threadId,
|
|
107
|
-
turnId: activeRun.turnId,
|
|
108
|
-
});
|
|
109
|
-
this.feed?.publish({
|
|
110
|
-
level: "info",
|
|
111
|
-
kind: source === "agent_session_prompt" ? "agent" : "comment",
|
|
112
|
-
projectId: issue.projectId,
|
|
113
|
-
issueKey: issue.issueKey,
|
|
114
|
-
stage: activeRun.runType,
|
|
115
|
-
status: "delivered",
|
|
116
|
-
summary: `Delivered agent input to active ${activeRun.runType} workflow`,
|
|
117
|
-
});
|
|
118
|
-
await params.emitActivity?.(buildPromptDeliveredThought(activeRun.runType), { ephemeral: true });
|
|
119
|
-
return { status: "steered", activeRunType: activeRun.runType };
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
-
this.logger.warn({ issueKey: issue.issueKey, error: message }, "Failed to deliver agent input to active Codex turn");
|
|
124
|
-
this.recordPromptDelivery({
|
|
125
|
-
issue,
|
|
126
|
-
source,
|
|
127
|
-
runId: activeRun.id,
|
|
128
|
-
runType: activeRun.runType,
|
|
129
|
-
status: "delivery_failed",
|
|
130
|
-
body,
|
|
131
|
-
primitive: "turn/steer",
|
|
132
|
-
threadId: activeRun.threadId,
|
|
133
|
-
turnId: activeRun.turnId,
|
|
134
|
-
error: message,
|
|
135
|
-
});
|
|
136
|
-
const queuedRunType = this.queueFollowUpEvent(issue, body, source, params.author, false);
|
|
137
|
-
this.feed?.publish({
|
|
138
|
-
level: "warn",
|
|
139
|
-
kind: source === "agent_session_prompt" ? "agent" : "comment",
|
|
140
|
-
projectId: issue.projectId,
|
|
141
|
-
issueKey: issue.issueKey,
|
|
142
|
-
stage: activeRun.runType,
|
|
143
|
-
status: "delivery_failed",
|
|
144
|
-
summary: `Could not deliver agent input to active ${activeRun.runType} workflow`,
|
|
145
|
-
});
|
|
146
|
-
await params.emitActivity?.(buildPromptDeliveryFailedActivity(activeRun.runType, message));
|
|
147
|
-
return { status: "delivery_failed", activeRunType: activeRun.runType, ...(queuedRunType ? { queuedRunType } : {}) };
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
async queueIdleInput(params) {
|
|
151
|
-
const originalIssue = params.issue;
|
|
152
|
-
let issue = originalIssue;
|
|
153
|
-
const replacementPrRequired = originalIssue.factoryState === "done" && originalIssue.prNumber !== undefined;
|
|
154
|
-
if (replacementPrRequired) {
|
|
155
|
-
issue = this.prepareReplacementWork(params.project, originalIssue);
|
|
156
|
-
}
|
|
157
|
-
const queuedRunType = this.queueFollowUpEvent(issue, params.body, params.source, params.author, params.directReply, replacementPrRequired ? originalIssue : undefined);
|
|
158
|
-
if (queuedRunType) {
|
|
159
|
-
await params.emitActivity?.(replacementPrRequired
|
|
160
|
-
? { type: "action", action: "Reopening", parameter: `completed PR #${originalIssue.prNumber} for replacement work` }
|
|
161
|
-
: buildPromptDeliveredThought(queuedRunType), { ephemeral: true });
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
await params.emitActivity?.(buildNonActionableFollowupActivity("unknown_needs_ack"));
|
|
165
|
-
}
|
|
166
|
-
return { status: queuedRunType ? "queued" : "ignored", ...(queuedRunType ? { queuedRunType } : {}) };
|
|
167
|
-
}
|
|
168
|
-
queueFollowUpEvent(issue, body, source, author, directReply, previousIssue) {
|
|
169
|
-
return this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
|
|
170
|
-
eventType: directReply ? "direct_reply" : source === "agent_session_prompt" ? "followup_prompt" : "followup_comment",
|
|
171
|
-
eventJson: JSON.stringify({
|
|
172
|
-
...(source === "agent_session_prompt" ? { text: body } : { body }),
|
|
173
|
-
source: source === "agent_session_prompt" ? "linear_agent_prompt" : "linear_comment",
|
|
174
|
-
...(author ? { author } : {}),
|
|
175
|
-
...(previousIssue?.prNumber !== undefined
|
|
176
|
-
? {
|
|
177
|
-
replacementPrRequired: true,
|
|
178
|
-
previousPrNumber: previousIssue.prNumber,
|
|
179
|
-
...(previousIssue.prUrl ? { previousPrUrl: previousIssue.prUrl } : {}),
|
|
180
|
-
...(previousIssue.prState ? { previousPrState: previousIssue.prState } : {}),
|
|
181
|
-
...(previousIssue.prHeadSha ? { previousPrHeadSha: previousIssue.prHeadSha } : {}),
|
|
182
|
-
}
|
|
183
|
-
: {}),
|
|
184
|
-
}),
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
prepareReplacementWork(project, issue) {
|
|
188
|
-
const issueRef = (issue.issueKey ?? issue.linearIssueId).replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
189
|
-
const suffix = Date.now().toString(36);
|
|
190
|
-
return this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
191
|
-
projectId: issue.projectId,
|
|
192
|
-
linearIssueId: issue.linearIssueId,
|
|
193
|
-
factoryState: "delegated",
|
|
194
|
-
branchName: `${project.branchPrefix}/${issueRef}-replacement-${suffix}`,
|
|
195
|
-
prNumber: null,
|
|
196
|
-
prUrl: null,
|
|
197
|
-
prState: null,
|
|
198
|
-
prIsDraft: null,
|
|
199
|
-
prHeadSha: null,
|
|
200
|
-
prAuthorLogin: null,
|
|
201
|
-
prReviewState: null,
|
|
202
|
-
prCheckStatus: null,
|
|
203
|
-
lastBlockingReviewHeadSha: null,
|
|
204
|
-
}) ?? issue;
|
|
205
|
-
}
|
|
206
|
-
async stopActiveRun(issue, run, body, source) {
|
|
207
|
-
if (run.threadId && run.turnId) {
|
|
208
|
-
try {
|
|
209
|
-
await this.codex.steerTurn({
|
|
210
|
-
threadId: run.threadId,
|
|
211
|
-
turnId: run.turnId,
|
|
212
|
-
input: "STOP: The user has requested you stop working immediately. Do not make further changes. Wrap up and exit.",
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
this.logger.warn({ issueKey: issue.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to steer Codex turn for stop request");
|
|
217
|
-
}
|
|
218
|
-
this.db.runs.finishRun(run.id, { status: "released", threadId: run.threadId, turnId: run.turnId });
|
|
219
|
-
}
|
|
220
|
-
this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
221
|
-
projectId: issue.projectId,
|
|
222
|
-
linearIssueId: issue.linearIssueId,
|
|
223
|
-
activeRunId: null,
|
|
224
|
-
factoryState: "awaiting_input",
|
|
225
|
-
});
|
|
226
|
-
this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
|
|
227
|
-
eventType: "stop_requested",
|
|
228
|
-
eventJson: JSON.stringify({ body, source }),
|
|
229
|
-
});
|
|
230
|
-
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
231
|
-
this.db.issueSessions.releaseIssueSessionLeaseRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
232
|
-
}
|
|
233
|
-
buildStatusActivity(issue, activeRun, peekPendingSessionWakeRunType, activityType) {
|
|
234
|
-
const latestRun = activeRun ?? this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
235
|
-
const latestEvent = this.db.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId).at(-1);
|
|
236
|
-
const statusNote = deriveIssueStatusNote({
|
|
237
|
-
issue,
|
|
238
|
-
latestRun,
|
|
239
|
-
latestEvent,
|
|
240
|
-
sessionSummary: extractLatestAssistantSummary(latestRun),
|
|
241
|
-
waitingReason: undefined,
|
|
242
|
-
});
|
|
243
|
-
const pendingRunType = peekPendingSessionWakeRunType?.(issue.projectId, issue.linearIssueId);
|
|
244
|
-
return buildFollowupStatusActivity({
|
|
245
|
-
issue,
|
|
246
|
-
...(statusNote ? { statusNote } : {}),
|
|
247
|
-
...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}),
|
|
248
|
-
...(pendingRunType ? { pendingRunType } : {}),
|
|
249
|
-
activityType,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
recordPromptDelivery(params) {
|
|
253
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.issue.projectId, params.issue.linearIssueId, {
|
|
254
|
-
projectId: params.issue.projectId,
|
|
255
|
-
linearIssueId: params.issue.linearIssueId,
|
|
256
|
-
eventType: "prompt_delivered",
|
|
257
|
-
eventJson: JSON.stringify({
|
|
258
|
-
source: params.source === "agent_session_prompt" ? "linear_agent_prompt" : "linear_comment",
|
|
259
|
-
runId: params.runId,
|
|
260
|
-
runType: params.runType,
|
|
261
|
-
status: params.status,
|
|
262
|
-
body: params.body,
|
|
263
|
-
primitive: params.primitive,
|
|
264
|
-
...(params.threadId ? { threadId: params.threadId } : {}),
|
|
265
|
-
...(params.turnId ? { turnId: params.turnId } : {}),
|
|
266
|
-
...(params.error ? { error: params.error } : {}),
|
|
267
|
-
}),
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
}
|
|
1
|
+
export { AgentInputService as CodexConversationAdapter, } from "./agent-input-service.js";
|
|
@@ -6,14 +6,12 @@ export class IssueSessionStore {
|
|
|
6
6
|
mapIssueSessionEventRow;
|
|
7
7
|
issues;
|
|
8
8
|
runs;
|
|
9
|
-
|
|
10
|
-
constructor(connection, mapIssueSessionRow, mapIssueSessionEventRow, issues, runs, deriveImplicitReactiveWake) {
|
|
9
|
+
constructor(connection, mapIssueSessionRow, mapIssueSessionEventRow, issues, runs) {
|
|
11
10
|
this.connection = connection;
|
|
12
11
|
this.mapIssueSessionRow = mapIssueSessionRow;
|
|
13
12
|
this.mapIssueSessionEventRow = mapIssueSessionEventRow;
|
|
14
13
|
this.issues = issues;
|
|
15
14
|
this.runs = runs;
|
|
16
|
-
this.deriveImplicitReactiveWake = deriveImplicitReactiveWake;
|
|
17
15
|
}
|
|
18
16
|
getIssueSession(projectId, linearIssueId) {
|
|
19
17
|
const row = this.connection
|
|
@@ -108,16 +106,7 @@ export class IssueSessionStore {
|
|
|
108
106
|
resumeThread: plan.resumeThread,
|
|
109
107
|
};
|
|
110
108
|
}
|
|
111
|
-
|
|
112
|
-
if (!implicitWake)
|
|
113
|
-
return undefined;
|
|
114
|
-
return {
|
|
115
|
-
eventIds: [],
|
|
116
|
-
runType: implicitWake.runType,
|
|
117
|
-
context: implicitWake.context,
|
|
118
|
-
wakeReason: implicitWake.wakeReason,
|
|
119
|
-
resumeThread: false,
|
|
120
|
-
};
|
|
109
|
+
return undefined;
|
|
121
110
|
}
|
|
122
111
|
acquireIssueSessionLease(params) {
|
|
123
112
|
const now = params.now ?? isoNow();
|
package/dist/db.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { deriveIssueSessionReactiveIntent, } from "./issue-session.js";
|
|
2
1
|
import {} from "./issue-session-events.js";
|
|
3
2
|
import { IssueStore } from "./db/issue-store.js";
|
|
4
3
|
import { IssueSessionStore } from "./db/issue-session-store.js";
|
|
@@ -11,84 +10,7 @@ import { runPatchRelayMigrations } from "./db/migrations.js";
|
|
|
11
10
|
import { SqliteConnection } from "./db/shared.js";
|
|
12
11
|
import { syncIssueSessionFromIssue } from "./issue-session-projector.js";
|
|
13
12
|
import { TrackedIssueQuery } from "./tracked-issue-query.js";
|
|
14
|
-
|
|
15
|
-
if (!raw)
|
|
16
|
-
return undefined;
|
|
17
|
-
try {
|
|
18
|
-
const parsed = JSON.parse(raw);
|
|
19
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
20
|
-
? parsed
|
|
21
|
-
: undefined;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function hasUnattemptedFailureSignature(issue, fallbackHeadSha) {
|
|
28
|
-
const signature = issue.lastGitHubFailureSignature;
|
|
29
|
-
if (!signature)
|
|
30
|
-
return false;
|
|
31
|
-
const headSha = issue.lastGitHubFailureHeadSha ?? fallbackHeadSha;
|
|
32
|
-
return issue.lastAttemptedFailureSignature !== signature
|
|
33
|
-
|| (headSha !== undefined && issue.lastAttemptedFailureHeadSha !== headSha);
|
|
34
|
-
}
|
|
35
|
-
function deriveImplicitReactiveWake(issue) {
|
|
36
|
-
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
37
|
-
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
38
|
-
activeRunId: issue.activeRunId,
|
|
39
|
-
prNumber: issue.prNumber,
|
|
40
|
-
prState: issue.prState,
|
|
41
|
-
prReviewState: issue.prReviewState,
|
|
42
|
-
prCheckStatus: issue.prCheckStatus,
|
|
43
|
-
latestFailureSource: issue.lastGitHubFailureSource,
|
|
44
|
-
});
|
|
45
|
-
if (!reactiveIntent)
|
|
46
|
-
return undefined;
|
|
47
|
-
if (reactiveIntent.runType === "ci_repair") {
|
|
48
|
-
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
|
|
49
|
-
const snapshot = parseObjectJson(issue.lastGitHubCiSnapshotJson);
|
|
50
|
-
const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
|
|
51
|
-
? failureContext.failureHeadSha
|
|
52
|
-
: issue.lastGitHubFailureHeadSha ?? issue.prHeadSha;
|
|
53
|
-
const failureSignature = issue.lastGitHubFailureSignature
|
|
54
|
-
?? (fallbackHeadSha ? `implicit_branch_ci::${fallbackHeadSha}` : undefined);
|
|
55
|
-
if (!failureSignature || issue.prState !== "open")
|
|
56
|
-
return undefined;
|
|
57
|
-
if (issue.lastAttemptedFailureSignature === failureSignature
|
|
58
|
-
&& (fallbackHeadSha === undefined || issue.lastAttemptedFailureHeadSha === fallbackHeadSha)) {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
return {
|
|
62
|
-
runType: reactiveIntent.runType,
|
|
63
|
-
wakeReason: reactiveIntent.wakeReason,
|
|
64
|
-
context: {
|
|
65
|
-
...failureContext,
|
|
66
|
-
failureSignature,
|
|
67
|
-
...(fallbackHeadSha ? { failureHeadSha: fallbackHeadSha } : {}),
|
|
68
|
-
...(issue.lastGitHubFailureCheckName ? { checkName: issue.lastGitHubFailureCheckName } : {}),
|
|
69
|
-
...(snapshot ? { ciSnapshot: snapshot } : {}),
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
if (reactiveIntent.runType === "queue_repair") {
|
|
74
|
-
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
|
|
75
|
-
const incidentContext = parseObjectJson(issue.lastQueueIncidentJson) ?? {};
|
|
76
|
-
const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
|
|
77
|
-
? failureContext.failureHeadSha
|
|
78
|
-
: undefined;
|
|
79
|
-
if (!hasUnattemptedFailureSignature(issue, fallbackHeadSha))
|
|
80
|
-
return undefined;
|
|
81
|
-
return {
|
|
82
|
-
runType: reactiveIntent.runType,
|
|
83
|
-
wakeReason: reactiveIntent.wakeReason,
|
|
84
|
-
context: {
|
|
85
|
-
...incidentContext,
|
|
86
|
-
...failureContext,
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
return undefined;
|
|
91
|
-
}
|
|
13
|
+
import { WorkflowWakeResolver } from "./workflow-wake-resolver.js";
|
|
92
14
|
export class PatchRelayDatabase {
|
|
93
15
|
connection;
|
|
94
16
|
linearInstallations;
|
|
@@ -97,6 +19,7 @@ export class PatchRelayDatabase {
|
|
|
97
19
|
webhookEvents;
|
|
98
20
|
issues;
|
|
99
21
|
issueSessions;
|
|
22
|
+
workflowWakes;
|
|
100
23
|
runs;
|
|
101
24
|
trackedIssues;
|
|
102
25
|
constructor(databasePath, wal) {
|
|
@@ -118,8 +41,9 @@ export class PatchRelayDatabase {
|
|
|
118
41
|
issue,
|
|
119
42
|
...(options ? { options } : {}),
|
|
120
43
|
}));
|
|
121
|
-
this.issueSessions = new IssueSessionStore(this.connection, mapIssueSessionRow, mapIssueSessionEventRow, this.issues, this.runs
|
|
122
|
-
this.
|
|
44
|
+
this.issueSessions = new IssueSessionStore(this.connection, mapIssueSessionRow, mapIssueSessionEventRow, this.issues, this.runs);
|
|
45
|
+
this.workflowWakes = new WorkflowWakeResolver(this.issues, this.issueSessions);
|
|
46
|
+
this.trackedIssues = new TrackedIssueQuery(this.issues, this.issueSessions, this.workflowWakes, this.runs);
|
|
123
47
|
}
|
|
124
48
|
runMigrations() {
|
|
125
49
|
runPatchRelayMigrations(this.connection);
|
|
@@ -513,7 +513,7 @@ export class IdleIssueReconciler {
|
|
|
513
513
|
}
|
|
514
514
|
if (issue.delegatedToPatchRelay
|
|
515
515
|
&& reactiveIntent?.runType === "review_fix"
|
|
516
|
-
&& this.db.
|
|
516
|
+
&& this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId) === undefined) {
|
|
517
517
|
this.logger.info({
|
|
518
518
|
issueKey: issue.issueKey,
|
|
519
519
|
prNumber: issue.prNumber,
|
|
@@ -147,7 +147,7 @@ export class InterruptedRunRecovery {
|
|
|
147
147
|
eventType: "delegated",
|
|
148
148
|
dedupeKey: `interrupted_implementation:implementation:${run.linearIssueId}`,
|
|
149
149
|
});
|
|
150
|
-
if (!this.db.
|
|
150
|
+
if (!this.db.workflowWakes.peekIssueWake(run.projectId, run.linearIssueId)) {
|
|
151
151
|
const failedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? refreshedIssue;
|
|
152
152
|
this.feed?.publish({
|
|
153
153
|
level: "error",
|
|
@@ -127,7 +127,7 @@ export class IssueOverviewQuery {
|
|
|
127
127
|
delegatedToPatchRelay: issueRecord?.delegatedToPatchRelay,
|
|
128
128
|
...(activeRun ? { activeRunId: activeRun.id } : {}),
|
|
129
129
|
blockedByCount: unresolvedBlockedBy.length,
|
|
130
|
-
hasPendingWake: this.db.
|
|
130
|
+
hasPendingWake: this.db.workflowWakes.peekIssueWake(session.projectId, session.linearIssueId) !== undefined,
|
|
131
131
|
hasLegacyPendingRun: issueRecord?.pendingRunType !== undefined,
|
|
132
132
|
orchestrationSettleUntil: issueRecord?.orchestrationSettleUntil,
|
|
133
133
|
...(session.prNumber !== undefined ? { prNumber: session.prNumber } : {}),
|
package/dist/run-wake-planner.js
CHANGED
|
@@ -5,7 +5,7 @@ export class RunWakePlanner {
|
|
|
5
5
|
this.db = db;
|
|
6
6
|
}
|
|
7
7
|
resolveRunWake(issue) {
|
|
8
|
-
const sessionWake = this.db.
|
|
8
|
+
const sessionWake = this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId);
|
|
9
9
|
if (!sessionWake)
|
|
10
10
|
return undefined;
|
|
11
11
|
return {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { buildOperatorRetryEvent } from "./operator-retry-event.js";
|
|
2
2
|
import { buildManualRetryAttemptReset, resolveRetryTarget } from "./manual-issue-actions.js";
|
|
3
3
|
export class ServiceIssueActions {
|
|
4
|
+
config;
|
|
4
5
|
db;
|
|
6
|
+
agentInput;
|
|
5
7
|
codex;
|
|
6
8
|
runtime;
|
|
7
9
|
feed;
|
|
8
10
|
logger;
|
|
9
|
-
constructor(db, codex, runtime, feed, logger) {
|
|
11
|
+
constructor(config, db, agentInput, codex, runtime, feed, logger) {
|
|
12
|
+
this.config = config;
|
|
10
13
|
this.db = db;
|
|
14
|
+
this.agentInput = agentInput;
|
|
11
15
|
this.codex = codex;
|
|
12
16
|
this.runtime = runtime;
|
|
13
17
|
this.feed = feed;
|
|
@@ -20,6 +24,9 @@ export class ServiceIssueActions {
|
|
|
20
24
|
if (!issue.delegatedToPatchRelay && !issue.activeRunId) {
|
|
21
25
|
return { error: "Issue is undelegated from PatchRelay; delegate it again before prompting work" };
|
|
22
26
|
}
|
|
27
|
+
const project = this.config.projects.find((entry) => entry.id === issue.projectId);
|
|
28
|
+
if (!project)
|
|
29
|
+
return { error: `Project ${issue.projectId} is not configured` };
|
|
23
30
|
this.feed.publish({
|
|
24
31
|
level: "info",
|
|
25
32
|
kind: "comment",
|
|
@@ -30,28 +37,18 @@ export class ServiceIssueActions {
|
|
|
30
37
|
summary: `Operator prompt (${source})`,
|
|
31
38
|
detail: text.slice(0, 200),
|
|
32
39
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
threadId: run.threadId,
|
|
44
|
-
turnId: run.turnId,
|
|
45
|
-
input: `Operator prompt (${source}):\n\n${text}`,
|
|
46
|
-
});
|
|
47
|
-
return { delivered: true };
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
51
|
-
this.logger.warn({ issueKey, error: msg }, "steerTurn failed, queuing prompt for next run");
|
|
52
|
-
this.queueOperatorPrompt(issue, text, source);
|
|
40
|
+
const result = await this.agentInput.deliverAgentInput({
|
|
41
|
+
project,
|
|
42
|
+
issue,
|
|
43
|
+
source: "patchrelay_operator_prompt",
|
|
44
|
+
body: text,
|
|
45
|
+
operatorSource: source,
|
|
46
|
+
});
|
|
47
|
+
if (result.status === "ignored")
|
|
48
|
+
return { delivered: false };
|
|
49
|
+
if (result.status === "delivery_failed" || result.status === "queued")
|
|
53
50
|
return { delivered: false, queued: true };
|
|
54
|
-
}
|
|
51
|
+
return { delivered: result.status === "steered" || result.status === "answered" || result.status === "stopped" };
|
|
55
52
|
}
|
|
56
53
|
async stopIssue(issueKey) {
|
|
57
54
|
const issue = this.db.issues.getIssueByKey(issueKey);
|
|
@@ -137,7 +134,7 @@ export class ServiceIssueActions {
|
|
|
137
134
|
status: "retry",
|
|
138
135
|
summary: `Retry queued: ${retryTarget.runType}`,
|
|
139
136
|
});
|
|
140
|
-
if (this.db.
|
|
137
|
+
if (this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId)) {
|
|
141
138
|
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
142
139
|
}
|
|
143
140
|
return { issueKey, runType: retryTarget.runType };
|
|
@@ -206,15 +203,6 @@ export class ServiceIssueActions {
|
|
|
206
203
|
...(run ? { releasedRunId: run.id } : {}),
|
|
207
204
|
};
|
|
208
205
|
}
|
|
209
|
-
queueOperatorPrompt(issue, text, source) {
|
|
210
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
211
|
-
projectId: issue.projectId,
|
|
212
|
-
linearIssueId: issue.linearIssueId,
|
|
213
|
-
eventType: "operator_prompt",
|
|
214
|
-
eventJson: JSON.stringify({ text, source }),
|
|
215
|
-
});
|
|
216
|
-
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
217
|
-
}
|
|
218
206
|
appendOperatorRetryEvent(issue, runType) {
|
|
219
207
|
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
220
208
|
projectId: issue.projectId,
|
|
@@ -90,7 +90,7 @@ export class ServiceStartupRecovery {
|
|
|
90
90
|
}
|
|
91
91
|
const unresolvedBlockers = this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
|
|
92
92
|
const latestRun = this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
93
|
-
const hasPendingWake = this.db.
|
|
93
|
+
const hasPendingWake = this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId) !== undefined;
|
|
94
94
|
const shouldRecoverPausedLocalWork = delegated
|
|
95
95
|
&& isResumablePausedLocalWork({
|
|
96
96
|
issue: {
|
|
@@ -147,7 +147,7 @@ export class ServiceStartupRecovery {
|
|
|
147
147
|
dedupeKey: `delegated:${issue.linearIssueId}`,
|
|
148
148
|
});
|
|
149
149
|
}
|
|
150
|
-
if (this.db.
|
|
150
|
+
if (this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId)) {
|
|
151
151
|
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
152
152
|
}
|
|
153
153
|
this.logger.info({
|
package/dist/service.js
CHANGED
|
@@ -13,6 +13,8 @@ import { WakeDispatcher } from "./wake-dispatcher.js";
|
|
|
13
13
|
import { WebhookHandler } from "./webhook-handler.js";
|
|
14
14
|
import { acceptIncomingWebhook } from "./service-webhooks.js";
|
|
15
15
|
import { parseStringArray, TrackedIssueListQuery } from "./tracked-issue-list-query.js";
|
|
16
|
+
import { AgentInputService } from "./agent-input-service.js";
|
|
17
|
+
import { CodexFollowupIntentClassifier } from "./followup-intent.js";
|
|
16
18
|
export class PatchRelayService {
|
|
17
19
|
config;
|
|
18
20
|
db;
|
|
@@ -52,9 +54,10 @@ export class PatchRelayService {
|
|
|
52
54
|
// orchestrator (its construction depends on the Codex client). All
|
|
53
55
|
// downstream consumers receive this single dispatcher instance.
|
|
54
56
|
const dispatcher = new WakeDispatcher(db, (projectId, issueId) => enqueueIssue(projectId, issueId), (projectId, issueId) => leaseRelease(projectId, issueId), logger, this.feed);
|
|
57
|
+
const agentInput = new AgentInputService(db, codex, dispatcher, logger, this.feed, new CodexFollowupIntentClassifier(codex, logger));
|
|
55
58
|
this.orchestrator = new RunOrchestrator(config, db, codex, this.linearProvider, (projectId, issueId) => enqueueIssue(projectId, issueId), dispatcher, logger, this.feed, this.configPath);
|
|
56
59
|
leaseRelease = (projectId, issueId) => this.orchestrator.leaseService.release(projectId, issueId);
|
|
57
|
-
this.webhookHandler = new WebhookHandler(config, db, this.linearProvider, codex, dispatcher, logger, this.feed);
|
|
60
|
+
this.webhookHandler = new WebhookHandler(config, db, this.linearProvider, codex, dispatcher, logger, this.feed, undefined, agentInput);
|
|
58
61
|
this.githubWebhookHandler = new GitHubWebhookHandler(config, db, this.linearProvider, dispatcher, logger, codex, this.feed);
|
|
59
62
|
const runtime = new ServiceRuntime(codex, logger, this.orchestrator, { listIssuesReadyForExecution: () => db.listIssuesReadyForExecution() }, this.webhookHandler, {
|
|
60
63
|
processIssue: async (item) => {
|
|
@@ -65,7 +68,7 @@ export class PatchRelayService {
|
|
|
65
68
|
this.oauthService = new LinearOAuthService(config, { linearInstallations: db.linearInstallations }, logger);
|
|
66
69
|
this.queryService = new IssueQueryService(db, codex, this.orchestrator);
|
|
67
70
|
this.runtime = runtime;
|
|
68
|
-
this.issueActions = new ServiceIssueActions(db, codex, runtime, this.feed, logger);
|
|
71
|
+
this.issueActions = new ServiceIssueActions(config, db, agentInput, codex, runtime, this.feed, logger);
|
|
69
72
|
this.startupRecovery = new ServiceStartupRecovery(db, this.linearProvider, this.orchestrator.linearSync, (projectId, issueId) => runtime.enqueueIssue(projectId, issueId), logger);
|
|
70
73
|
this.trackedIssueListQuery = new TrackedIssueListQuery(db);
|
|
71
74
|
// Optional GitHub App token management for bot identity
|
|
@@ -155,7 +155,7 @@ export class TrackedIssueListQuery {
|
|
|
155
155
|
const blockedByCount = Number(row.blocked_by_count ?? 0);
|
|
156
156
|
const hasPendingSessionEvents = Number(row.pending_session_event_count ?? 0) > 0;
|
|
157
157
|
const hasPendingWake = hasPendingSessionEvents
|
|
158
|
-
|| this.db.
|
|
158
|
+
|| this.db.workflowWakes.peekIssueWake(String(row.project_id), String(row.linear_issue_id)) !== undefined;
|
|
159
159
|
const detachedActiveRun = hasDetachedActiveLatestRun({
|
|
160
160
|
activeRunId: row.active_run_type !== null ? 1 : undefined,
|
|
161
161
|
latestRun: row.latest_run_status !== null
|
|
@@ -4,10 +4,12 @@ import { resolveEffectiveActiveRun } from "./effective-active-run.js";
|
|
|
4
4
|
export class TrackedIssueQuery {
|
|
5
5
|
issues;
|
|
6
6
|
issueSessions;
|
|
7
|
+
workflowWakes;
|
|
7
8
|
runs;
|
|
8
|
-
constructor(issues, issueSessions, runs) {
|
|
9
|
+
constructor(issues, issueSessions, workflowWakes, runs) {
|
|
9
10
|
this.issues = issues;
|
|
10
11
|
this.issueSessions = issueSessions;
|
|
12
|
+
this.workflowWakes = workflowWakes;
|
|
11
13
|
this.runs = runs;
|
|
12
14
|
}
|
|
13
15
|
listIssuesReadyForExecution() {
|
|
@@ -20,7 +22,7 @@ export class TrackedIssueQuery {
|
|
|
20
22
|
}),
|
|
21
23
|
activeRunId: issue.activeRunId,
|
|
22
24
|
blockedByCount: this.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId),
|
|
23
|
-
hasPendingWake: this.
|
|
25
|
+
hasPendingWake: this.workflowWakes.hasPendingWake(issue.projectId, issue.linearIssueId),
|
|
24
26
|
hasLegacyPendingRun: issue.pendingRunType !== undefined,
|
|
25
27
|
prNumber: issue.prNumber,
|
|
26
28
|
prState: issue.prState,
|
|
@@ -38,7 +40,7 @@ export class TrackedIssueQuery {
|
|
|
38
40
|
issue,
|
|
39
41
|
session: this.issueSessions.getIssueSession(issue.projectId, issue.linearIssueId),
|
|
40
42
|
blockedBy: this.issues.listIssueDependencies(issue.projectId, issue.linearIssueId),
|
|
41
|
-
hasPendingWake: this.
|
|
43
|
+
hasPendingWake: this.workflowWakes.hasPendingWake(issue.projectId, issue.linearIssueId),
|
|
42
44
|
latestRun: this.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId),
|
|
43
45
|
latestEvent: this.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1),
|
|
44
46
|
});
|
package/dist/wake-dispatcher.js
CHANGED
|
@@ -69,7 +69,7 @@ export class WakeDispatcher {
|
|
|
69
69
|
const issue = this.db.issues.getIssue(projectId, linearIssueId);
|
|
70
70
|
if (issue?.activeRunId !== undefined)
|
|
71
71
|
return undefined;
|
|
72
|
-
const wake = this.db.
|
|
72
|
+
const wake = this.db.workflowWakes.peekIssueWake(projectId, linearIssueId);
|
|
73
73
|
// Fall back to the legacy pending_run_type column. The orchestrator
|
|
74
74
|
// materializes it into a real event at run time, but the poke still
|
|
75
75
|
// needs to happen now so the orchestrator gets called at all.
|
|
@@ -97,7 +97,7 @@ export class WakeDispatcher {
|
|
|
97
97
|
// check paths publish their own more-specific event and pass false.
|
|
98
98
|
releaseRunAndDispatch(params) {
|
|
99
99
|
this.releaseLease(params.run.projectId, params.run.linearIssueId);
|
|
100
|
-
const wake = this.db.
|
|
100
|
+
const wake = this.db.workflowWakes.peekIssueWake(params.run.projectId, params.run.linearIssueId);
|
|
101
101
|
if (!wake)
|
|
102
102
|
return undefined;
|
|
103
103
|
this.enqueueIssue(params.run.projectId, params.run.linearIssueId);
|
package/dist/webhook-handler.js
CHANGED
|
@@ -13,7 +13,7 @@ import { safeJsonParse, sanitizeDiagnosticText } from "./utils.js";
|
|
|
13
13
|
import { extractLatestAssistantSummary } from "./issue-session-events.js";
|
|
14
14
|
import { WakeDispatcher } from "./wake-dispatcher.js";
|
|
15
15
|
import { CodexFollowupIntentClassifier } from "./followup-intent.js";
|
|
16
|
-
import {
|
|
16
|
+
import { AgentInputService } from "./agent-input-service.js";
|
|
17
17
|
export class WebhookHandler {
|
|
18
18
|
config;
|
|
19
19
|
db;
|
|
@@ -30,7 +30,7 @@ export class WebhookHandler {
|
|
|
30
30
|
dependencyReadinessHandler;
|
|
31
31
|
linearSync;
|
|
32
32
|
wakeDispatcher;
|
|
33
|
-
constructor(config, db, linearProvider, codex, wakeDispatcherOrEnqueueIssue, logger, feed, followupClassifier) {
|
|
33
|
+
constructor(config, db, linearProvider, codex, wakeDispatcherOrEnqueueIssue, logger, feed, followupClassifier, agentInput) {
|
|
34
34
|
this.config = config;
|
|
35
35
|
this.db = db;
|
|
36
36
|
this.linearProvider = linearProvider;
|
|
@@ -48,9 +48,9 @@ export class WebhookHandler {
|
|
|
48
48
|
this.issueRemovalHandler = new IssueRemovalHandler(db, feed);
|
|
49
49
|
this.linearSync = new LinearSessionSync(config, db, linearProvider, logger, feed);
|
|
50
50
|
const intentClassifier = followupClassifier ?? new CodexFollowupIntentClassifier(codex, logger);
|
|
51
|
-
const
|
|
52
|
-
this.commentWakeHandler = new CommentWakeHandler(db, this.wakeDispatcher, feed,
|
|
53
|
-
this.agentSessionHandler = new AgentSessionHandler(config, db, linearProvider, codex, this.wakeDispatcher, logger, feed,
|
|
51
|
+
const agentInputService = agentInput ?? new AgentInputService(db, codex, this.wakeDispatcher, logger, feed, intentClassifier);
|
|
52
|
+
this.commentWakeHandler = new CommentWakeHandler(db, this.wakeDispatcher, feed, agentInputService, (issue, content, options) => this.linearSync.emitActivity(issue, content, options));
|
|
53
|
+
this.agentSessionHandler = new AgentSessionHandler(config, db, linearProvider, codex, this.wakeDispatcher, logger, feed, agentInputService);
|
|
54
54
|
this.desiredStageRecorder = new DesiredStageRecorder(db, linearProvider, this.wakeDispatcher, feed);
|
|
55
55
|
this.contextLoader = new WebhookContextLoader(config, linearProvider);
|
|
56
56
|
this.dependencyReadinessHandler = new DependencyReadinessHandler(db, this.wakeDispatcher, (projectId, issueId) => this.peekPendingSessionWakeRunType(projectId, issueId));
|
|
@@ -232,7 +232,7 @@ export class WebhookHandler {
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
peekPendingSessionWakeRunType(projectId, issueId) {
|
|
235
|
-
return this.db.
|
|
235
|
+
return this.db.workflowWakes.peekIssueWake(projectId, issueId)?.runType;
|
|
236
236
|
}
|
|
237
237
|
enqueuePendingSessionWake(projectId, issueId) {
|
|
238
238
|
return this.wakeDispatcher.dispatchIfWakePending(projectId, issueId);
|
|
@@ -17,8 +17,8 @@ export class AgentSessionHandler {
|
|
|
17
17
|
wakeDispatcher;
|
|
18
18
|
logger;
|
|
19
19
|
feed;
|
|
20
|
-
|
|
21
|
-
constructor(config, db, linearProvider, codex, wakeDispatcher, logger, feed,
|
|
20
|
+
agentInput;
|
|
21
|
+
constructor(config, db, linearProvider, codex, wakeDispatcher, logger, feed, agentInput) {
|
|
22
22
|
this.config = config;
|
|
23
23
|
this.db = db;
|
|
24
24
|
this.linearProvider = linearProvider;
|
|
@@ -26,7 +26,7 @@ export class AgentSessionHandler {
|
|
|
26
26
|
this.wakeDispatcher = wakeDispatcher;
|
|
27
27
|
this.logger = logger;
|
|
28
28
|
this.feed = feed;
|
|
29
|
-
this.
|
|
29
|
+
this.agentInput = agentInput;
|
|
30
30
|
}
|
|
31
31
|
async acknowledgeCreated(normalized) {
|
|
32
32
|
if (normalized.triggerEvent !== "agentSessionCreated" || !normalized.agentSession?.id || !normalized.issue) {
|
|
@@ -126,11 +126,11 @@ export class AgentSessionHandler {
|
|
|
126
126
|
}
|
|
127
127
|
const promptBody = normalized.agentSession.promptBody?.trim();
|
|
128
128
|
const directReply = promptBody && existingIssue ? params.isDirectReplyToOutstandingQuestion(existingIssue) : false;
|
|
129
|
-
if (promptBody && existingIssue && this.
|
|
130
|
-
const result = await this.
|
|
129
|
+
if (promptBody && existingIssue && this.agentInput) {
|
|
130
|
+
const result = await this.agentInput.deliverAgentInput({
|
|
131
131
|
project,
|
|
132
132
|
issue: existingIssue,
|
|
133
|
-
source: "
|
|
133
|
+
source: "linear_agent_session",
|
|
134
134
|
body: promptBody,
|
|
135
135
|
directReply,
|
|
136
136
|
emitActivity: (content, options) => this.publishAgentActivity(linear, normalized.agentSession.id, content, options),
|
|
@@ -4,13 +4,13 @@ export class CommentWakeHandler {
|
|
|
4
4
|
db;
|
|
5
5
|
wakeDispatcher;
|
|
6
6
|
feed;
|
|
7
|
-
|
|
7
|
+
agentInput;
|
|
8
8
|
emitLinearActivity;
|
|
9
|
-
constructor(db, wakeDispatcher, feed,
|
|
9
|
+
constructor(db, wakeDispatcher, feed, agentInput, emitLinearActivity) {
|
|
10
10
|
this.db = db;
|
|
11
11
|
this.wakeDispatcher = wakeDispatcher;
|
|
12
12
|
this.feed = feed;
|
|
13
|
-
this.
|
|
13
|
+
this.agentInput = agentInput;
|
|
14
14
|
this.emitLinearActivity = emitLinearActivity;
|
|
15
15
|
}
|
|
16
16
|
async handle(params) {
|
|
@@ -64,10 +64,10 @@ export class CommentWakeHandler {
|
|
|
64
64
|
});
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
const result = await this.
|
|
67
|
+
const result = await this.agentInput?.deliverAgentInput({
|
|
68
68
|
project,
|
|
69
69
|
issue,
|
|
70
|
-
source: "
|
|
70
|
+
source: "linear_addressed_comment",
|
|
71
71
|
body: addressedText,
|
|
72
72
|
author: normalized.comment.userName,
|
|
73
73
|
directReply: params.isDirectReplyToOutstandingQuestion(issue),
|
|
@@ -29,7 +29,7 @@ export class DesiredStageRecorder {
|
|
|
29
29
|
const latestRun = existingIssue ? this.db.runs.getLatestRunForIssue(params.project.id, normalizedIssue.id) : undefined;
|
|
30
30
|
const triggerAllowed = triggerEventAllowed(params.project, params.normalized.triggerEvent);
|
|
31
31
|
const incomingAgentSessionId = params.normalized.agentSession?.id;
|
|
32
|
-
const hasPendingWake =
|
|
32
|
+
const hasPendingWake = params.peekPendingSessionWakeRunType(params.project.id, normalizedIssue.id) !== undefined;
|
|
33
33
|
if (!existingIssue && !isDelegatedToPatchRelay(this.db, params.project, normalizedIssue) && !incomingAgentSessionId) {
|
|
34
34
|
return { issue: undefined, wakeRunType: undefined, delegated: false };
|
|
35
35
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
2
|
+
function parseObjectJson(raw) {
|
|
3
|
+
if (!raw)
|
|
4
|
+
return undefined;
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(raw);
|
|
7
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
8
|
+
? parsed
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function hasUnattemptedFailureSignature(issue, fallbackHeadSha) {
|
|
16
|
+
const signature = issue.lastGitHubFailureSignature;
|
|
17
|
+
if (!signature)
|
|
18
|
+
return false;
|
|
19
|
+
const headSha = issue.lastGitHubFailureHeadSha ?? fallbackHeadSha;
|
|
20
|
+
return issue.lastAttemptedFailureSignature !== signature
|
|
21
|
+
|| (headSha !== undefined && issue.lastAttemptedFailureHeadSha !== headSha);
|
|
22
|
+
}
|
|
23
|
+
export function deriveImplicitReactiveWake(issue) {
|
|
24
|
+
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
25
|
+
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
26
|
+
activeRunId: issue.activeRunId,
|
|
27
|
+
prNumber: issue.prNumber,
|
|
28
|
+
prState: issue.prState,
|
|
29
|
+
prReviewState: issue.prReviewState,
|
|
30
|
+
prCheckStatus: issue.prCheckStatus,
|
|
31
|
+
latestFailureSource: issue.lastGitHubFailureSource,
|
|
32
|
+
});
|
|
33
|
+
if (!reactiveIntent)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (reactiveIntent.runType === "ci_repair") {
|
|
36
|
+
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
|
|
37
|
+
const snapshot = parseObjectJson(issue.lastGitHubCiSnapshotJson);
|
|
38
|
+
const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
|
|
39
|
+
? failureContext.failureHeadSha
|
|
40
|
+
: issue.lastGitHubFailureHeadSha ?? issue.prHeadSha;
|
|
41
|
+
const failureSignature = issue.lastGitHubFailureSignature
|
|
42
|
+
?? (fallbackHeadSha ? `implicit_branch_ci::${fallbackHeadSha}` : undefined);
|
|
43
|
+
if (!failureSignature || issue.prState !== "open")
|
|
44
|
+
return undefined;
|
|
45
|
+
if (issue.lastAttemptedFailureSignature === failureSignature
|
|
46
|
+
&& (fallbackHeadSha === undefined || issue.lastAttemptedFailureHeadSha === fallbackHeadSha)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
runType: reactiveIntent.runType,
|
|
51
|
+
wakeReason: reactiveIntent.wakeReason,
|
|
52
|
+
context: {
|
|
53
|
+
...failureContext,
|
|
54
|
+
failureSignature,
|
|
55
|
+
...(fallbackHeadSha ? { failureHeadSha: fallbackHeadSha } : {}),
|
|
56
|
+
...(issue.lastGitHubFailureCheckName ? { checkName: issue.lastGitHubFailureCheckName } : {}),
|
|
57
|
+
...(snapshot ? { ciSnapshot: snapshot } : {}),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (reactiveIntent.runType === "queue_repair") {
|
|
62
|
+
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
|
|
63
|
+
const incidentContext = parseObjectJson(issue.lastQueueIncidentJson) ?? {};
|
|
64
|
+
const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
|
|
65
|
+
? failureContext.failureHeadSha
|
|
66
|
+
: undefined;
|
|
67
|
+
if (!hasUnattemptedFailureSignature(issue, fallbackHeadSha))
|
|
68
|
+
return undefined;
|
|
69
|
+
return {
|
|
70
|
+
runType: reactiveIntent.runType,
|
|
71
|
+
wakeReason: reactiveIntent.wakeReason,
|
|
72
|
+
context: {
|
|
73
|
+
...incidentContext,
|
|
74
|
+
...failureContext,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
export class WorkflowWakeResolver {
|
|
81
|
+
issues;
|
|
82
|
+
issueSessions;
|
|
83
|
+
constructor(issues, issueSessions) {
|
|
84
|
+
this.issues = issues;
|
|
85
|
+
this.issueSessions = issueSessions;
|
|
86
|
+
}
|
|
87
|
+
peekIssueWake(projectId, linearIssueId) {
|
|
88
|
+
const explicitWake = this.issueSessions.peekIssueSessionWake(projectId, linearIssueId);
|
|
89
|
+
if (explicitWake)
|
|
90
|
+
return explicitWake;
|
|
91
|
+
const issue = this.issues.getIssue(projectId, linearIssueId);
|
|
92
|
+
if (!issue)
|
|
93
|
+
return undefined;
|
|
94
|
+
const implicitWake = deriveImplicitReactiveWake(issue);
|
|
95
|
+
if (!implicitWake)
|
|
96
|
+
return undefined;
|
|
97
|
+
return {
|
|
98
|
+
eventIds: [],
|
|
99
|
+
runType: implicitWake.runType,
|
|
100
|
+
context: implicitWake.context,
|
|
101
|
+
wakeReason: implicitWake.wakeReason,
|
|
102
|
+
resumeThread: false,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
hasPendingWake(projectId, linearIssueId) {
|
|
106
|
+
return this.peekIssueWake(projectId, linearIssueId) !== undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|