patchrelay 0.7.9 → 0.7.10
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/build-info.json
CHANGED
|
@@ -3,6 +3,7 @@ import { syncFailedStageToLinear } from "./stage-failure.js";
|
|
|
3
3
|
import { buildFailedStageReport } from "./stage-reporting.js";
|
|
4
4
|
import { StageLifecyclePublisher } from "./stage-lifecycle-publisher.js";
|
|
5
5
|
import { StageTurnInputDispatcher } from "./stage-turn-input-dispatcher.js";
|
|
6
|
+
import { safeJsonParse } from "./utils.js";
|
|
6
7
|
import { WorktreeManager } from "./worktree-manager.js";
|
|
7
8
|
export class ServiceStageRunner {
|
|
8
9
|
config;
|
|
@@ -86,11 +87,16 @@ export class ServiceStageRunner {
|
|
|
86
87
|
});
|
|
87
88
|
await this.lifecyclePublisher.markStageActive(project, claim.issue, claim.stageRun);
|
|
88
89
|
threadLaunch = await this.launchStageThread(item.projectId, item.issueId, claim.stageRun.id, plan.worktreePath, issue.issueKey);
|
|
90
|
+
const pendingLaunchInput = this.collectPendingLaunchInput(item.projectId, item.issueId);
|
|
91
|
+
const initialTurnInput = pendingLaunchInput.combinedInput
|
|
92
|
+
? [plan.prompt, "", pendingLaunchInput.combinedInput].join("\n")
|
|
93
|
+
: plan.prompt;
|
|
89
94
|
turn = await this.codex.startTurn({
|
|
90
95
|
threadId: threadLaunch.threadId,
|
|
91
96
|
cwd: plan.worktreePath,
|
|
92
|
-
input:
|
|
97
|
+
input: initialTurnInput,
|
|
93
98
|
});
|
|
99
|
+
this.completeDeliveredLaunchInput(pendingLaunchInput.obligationIds, claim.stageRun.id, threadLaunch.threadId, turn.turnId);
|
|
94
100
|
}
|
|
95
101
|
catch (error) {
|
|
96
102
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -122,19 +128,8 @@ export class ServiceStageRunner {
|
|
|
122
128
|
turnId: turn.turnId,
|
|
123
129
|
});
|
|
124
130
|
this.inputDispatcher.routePendingInputs(claim.stageRun, threadLaunch.threadId, turn.turnId);
|
|
125
|
-
await this.inputDispatcher.flush({
|
|
126
|
-
id: claim.stageRun.id,
|
|
127
|
-
projectId: claim.stageRun.projectId,
|
|
128
|
-
linearIssueId: claim.stageRun.linearIssueId,
|
|
129
|
-
threadId: threadLaunch.threadId,
|
|
130
|
-
turnId: turn.turnId,
|
|
131
|
-
}, {
|
|
132
|
-
logFailures: true,
|
|
133
|
-
failureMessage: "Failed to deliver queued Linear comment during stage startup",
|
|
134
|
-
...(claim.issue.issueKey ? { issueKey: claim.issue.issueKey } : {}),
|
|
135
|
-
});
|
|
136
131
|
const deliveredToSession = await this.lifecyclePublisher.publishStageStarted(claim.issue, claim.stageRun.stage);
|
|
137
|
-
if (!deliveredToSession) {
|
|
132
|
+
if (!deliveredToSession && !claim.issue.activeAgentSessionId) {
|
|
138
133
|
await this.lifecyclePublisher.refreshRunningStatusComment(item.projectId, item.issueId, claim.stageRun.id, issue.issueKey);
|
|
139
134
|
}
|
|
140
135
|
this.logger.info({
|
|
@@ -156,6 +151,39 @@ export class ServiceStageRunner {
|
|
|
156
151
|
detail: `Turn ${turn.turnId} is running in ${plan.branchName}.`,
|
|
157
152
|
});
|
|
158
153
|
}
|
|
154
|
+
collectPendingLaunchInput(projectId, issueId) {
|
|
155
|
+
const obligationIds = [];
|
|
156
|
+
const bodies = [];
|
|
157
|
+
for (const obligation of this.stores.obligations.listPendingObligations({ kind: "deliver_turn_input" })) {
|
|
158
|
+
if (obligation.projectId !== projectId ||
|
|
159
|
+
obligation.linearIssueId !== issueId ||
|
|
160
|
+
!obligation.source.startsWith("linear-agent-launch:")) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const payload = safeJsonParse(obligation.payloadJson);
|
|
164
|
+
const body = payload?.body?.trim();
|
|
165
|
+
if (!body) {
|
|
166
|
+
this.stores.obligations.markObligationStatus(obligation.id, "failed", "obligation payload had no deliverable body");
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
obligationIds.push(obligation.id);
|
|
170
|
+
bodies.push(body);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
...(bodies.length > 0 ? { combinedInput: bodies.join("\n\n") } : {}),
|
|
174
|
+
obligationIds,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
completeDeliveredLaunchInput(obligationIds, runLeaseId, threadId, turnId) {
|
|
178
|
+
for (const obligationId of obligationIds) {
|
|
179
|
+
this.stores.obligations.updateObligationRouting(obligationId, {
|
|
180
|
+
runLeaseId,
|
|
181
|
+
threadId,
|
|
182
|
+
turnId,
|
|
183
|
+
});
|
|
184
|
+
this.stores.obligations.markObligationStatus(obligationId, "completed");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
159
187
|
async ensureLaunchIssueMirror(project, linearIssueId, _desiredStage, _desiredWebhookId) {
|
|
160
188
|
const existing = this.stores.issueWorkflows.getTrackedIssue(project.id, linearIssueId);
|
|
161
189
|
if (existing?.issueKey && existing.title && existing.issueUrl && existing.currentLinearState) {
|
|
@@ -45,7 +45,7 @@ export class ServiceWebhookProcessor {
|
|
|
45
45
|
this.markEventReceiptProcessed(event.webhookId, "failed");
|
|
46
46
|
throw new Error(`Stored webhook payload is invalid JSON: event ${webhookEventId}`);
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
let normalized = normalizeWebhook({
|
|
49
49
|
webhookId: event.webhookId,
|
|
50
50
|
payload,
|
|
51
51
|
});
|
|
@@ -69,12 +69,25 @@ export class ServiceWebhookProcessor {
|
|
|
69
69
|
this.markEventReceiptProcessed(event.webhookId, "processed");
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
let project = resolveProject(this.config, normalized.issue);
|
|
73
73
|
if (!project) {
|
|
74
|
+
const routed = await this.tryHydrateProjectRoute(normalized);
|
|
75
|
+
if (routed) {
|
|
76
|
+
normalized = routed.normalized;
|
|
77
|
+
project = routed.project;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!project) {
|
|
81
|
+
const unresolvedIssue = normalized.issue;
|
|
82
|
+
if (!unresolvedIssue) {
|
|
83
|
+
this.stores.webhookEvents.markWebhookProcessed(webhookEventId, "failed");
|
|
84
|
+
this.markEventReceiptProcessed(event.webhookId, "failed");
|
|
85
|
+
throw new Error(`Normalized issue context disappeared before routing webhook ${event.webhookId}`);
|
|
86
|
+
}
|
|
74
87
|
this.feed?.publish({
|
|
75
88
|
level: "warn",
|
|
76
89
|
kind: "webhook",
|
|
77
|
-
issueKey:
|
|
90
|
+
issueKey: unresolvedIssue.identifier,
|
|
78
91
|
status: "ignored",
|
|
79
92
|
summary: "Ignored webhook with no matching project route",
|
|
80
93
|
detail: normalized.triggerEvent,
|
|
@@ -82,21 +95,27 @@ export class ServiceWebhookProcessor {
|
|
|
82
95
|
this.logger.info({
|
|
83
96
|
webhookEventId,
|
|
84
97
|
webhookId: event.webhookId,
|
|
85
|
-
issueKey:
|
|
86
|
-
issueId:
|
|
87
|
-
teamId:
|
|
88
|
-
teamKey:
|
|
98
|
+
issueKey: unresolvedIssue.identifier,
|
|
99
|
+
issueId: unresolvedIssue.id,
|
|
100
|
+
teamId: unresolvedIssue.teamId,
|
|
101
|
+
teamKey: unresolvedIssue.teamKey,
|
|
89
102
|
triggerEvent: normalized.triggerEvent,
|
|
90
103
|
}, "Ignoring webhook because no project route matched the Linear issue");
|
|
91
104
|
this.stores.webhookEvents.markWebhookProcessed(webhookEventId, "processed");
|
|
92
105
|
this.markEventReceiptProcessed(event.webhookId, "processed");
|
|
93
106
|
return;
|
|
94
107
|
}
|
|
108
|
+
const routedIssue = normalized.issue;
|
|
109
|
+
if (!routedIssue) {
|
|
110
|
+
this.stores.webhookEvents.markWebhookProcessed(webhookEventId, "failed");
|
|
111
|
+
this.markEventReceiptProcessed(event.webhookId, "failed");
|
|
112
|
+
throw new Error(`Normalized issue context disappeared while routing webhook ${event.webhookId}`);
|
|
113
|
+
}
|
|
95
114
|
if (!trustedActorAllowed(project, normalized.actor)) {
|
|
96
115
|
this.feed?.publish({
|
|
97
116
|
level: "warn",
|
|
98
117
|
kind: "webhook",
|
|
99
|
-
issueKey:
|
|
118
|
+
issueKey: routedIssue.identifier,
|
|
100
119
|
projectId: project.id,
|
|
101
120
|
status: "ignored",
|
|
102
121
|
summary: "Ignored webhook from an untrusted actor",
|
|
@@ -111,14 +130,14 @@ export class ServiceWebhookProcessor {
|
|
|
111
130
|
actorEmail: normalized.actor?.email,
|
|
112
131
|
}, "Ignoring webhook from untrusted Linear actor");
|
|
113
132
|
this.stores.webhookEvents.markWebhookProcessed(webhookEventId, "processed");
|
|
114
|
-
this.assignEventReceiptContext(event.webhookId, project.id,
|
|
133
|
+
this.assignEventReceiptContext(event.webhookId, project.id, routedIssue.id);
|
|
115
134
|
this.markEventReceiptProcessed(event.webhookId, "processed");
|
|
116
135
|
return;
|
|
117
136
|
}
|
|
118
137
|
this.stores.webhookEvents.assignWebhookProject(webhookEventId, project.id);
|
|
119
|
-
const receipt = this.ensureEventReceipt(event, project.id,
|
|
138
|
+
const receipt = this.ensureEventReceipt(event, project.id, routedIssue.id);
|
|
120
139
|
const hydrated = await this.hydrateIssueContext(project.id, normalized);
|
|
121
|
-
const hydratedIssue = hydrated.issue ??
|
|
140
|
+
const hydratedIssue = hydrated.issue ?? routedIssue;
|
|
122
141
|
const issueState = this.desiredStageRecorder.record(project, hydrated, receipt ? { eventReceiptId: receipt.id } : undefined);
|
|
123
142
|
const observation = describeWebhookObservation(hydrated, issueState.delegatedToPatchRelay);
|
|
124
143
|
if (observation) {
|
|
@@ -229,6 +248,43 @@ export class ServiceWebhookProcessor {
|
|
|
229
248
|
return normalized;
|
|
230
249
|
}
|
|
231
250
|
}
|
|
251
|
+
async tryHydrateProjectRoute(normalized) {
|
|
252
|
+
if (!normalized.issue) {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
if (normalized.triggerEvent !== "agentSessionCreated" && normalized.triggerEvent !== "agentPrompted") {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
for (const candidate of this.config.projects) {
|
|
259
|
+
const linear = await this.linearProvider.forProject(candidate.id);
|
|
260
|
+
if (!linear) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const liveIssue = await linear.getIssue(normalized.issue.id);
|
|
265
|
+
const hydrated = {
|
|
266
|
+
...normalized,
|
|
267
|
+
issue: mergeIssueMetadata(normalized.issue, liveIssue),
|
|
268
|
+
};
|
|
269
|
+
const resolved = resolveProject(this.config, hydrated.issue);
|
|
270
|
+
if (resolved) {
|
|
271
|
+
return {
|
|
272
|
+
project: resolved,
|
|
273
|
+
normalized: hydrated,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
this.logger.debug({
|
|
279
|
+
candidateProjectId: candidate.id,
|
|
280
|
+
issueId: normalized.issue.id,
|
|
281
|
+
triggerEvent: normalized.triggerEvent,
|
|
282
|
+
error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
|
|
283
|
+
}, "Failed to hydrate Linear issue context while resolving project route");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
232
288
|
assignEventReceiptContext(webhookId, projectId, linearIssueId) {
|
|
233
289
|
const receipt = this.lookupEventReceipt(webhookId);
|
|
234
290
|
if (!receipt) {
|
package/dist/stage-failure.js
CHANGED
|
@@ -71,7 +71,7 @@ export async function syncFailedStageToLinear(params) {
|
|
|
71
71
|
.then(() => true)
|
|
72
72
|
.catch(() => false)) || deliveredToSession;
|
|
73
73
|
}
|
|
74
|
-
if (!deliveredToSession) {
|
|
74
|
+
if (!deliveredToSession && !params.issue.activeAgentSessionId) {
|
|
75
75
|
const result = await linear
|
|
76
76
|
.upsertIssueComment({
|
|
77
77
|
issueId: params.stageRun.linearIssueId,
|
|
@@ -84,7 +84,7 @@ export class StageLifecyclePublisher {
|
|
|
84
84
|
await linear.createAgentActivity({
|
|
85
85
|
agentSessionId: issue.activeAgentSessionId,
|
|
86
86
|
content: {
|
|
87
|
-
type: "
|
|
87
|
+
type: "response",
|
|
88
88
|
body: `PatchRelay started the ${stage} workflow and is working in the background.`,
|
|
89
89
|
},
|
|
90
90
|
});
|
|
@@ -155,7 +155,7 @@ export class StageLifecyclePublisher {
|
|
|
155
155
|
type: "elicitation",
|
|
156
156
|
body: `PatchRelay finished the ${stageRun.stage} workflow. Move the issue to its next workflow state or leave a follow-up prompt to continue.`,
|
|
157
157
|
})) || deliveredToSession;
|
|
158
|
-
if (!deliveredToSession) {
|
|
158
|
+
if (!deliveredToSession && !refreshedIssue.activeAgentSessionId) {
|
|
159
159
|
const result = await linear.upsertIssueComment({
|
|
160
160
|
issueId: stageRun.linearIssueId,
|
|
161
161
|
...(refreshedIssue.statusCommentId ? { commentId: refreshedIssue.statusCommentId } : {}),
|
|
@@ -66,7 +66,7 @@ export class AgentSessionWebhookHandler {
|
|
|
66
66
|
if (desiredStage) {
|
|
67
67
|
await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildPreparingSessionPlan(desiredStage)));
|
|
68
68
|
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
69
|
-
type: "
|
|
69
|
+
type: "response",
|
|
70
70
|
body: `PatchRelay started working on the ${desiredStage} workflow and is preparing the workspace.`,
|
|
71
71
|
}, { ephemeral: false });
|
|
72
72
|
return;
|
|
@@ -137,7 +137,7 @@ export class AgentSessionWebhookHandler {
|
|
|
137
137
|
if (!activeRunLease && desiredStage) {
|
|
138
138
|
await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildPreparingSessionPlan(desiredStage)));
|
|
139
139
|
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
140
|
-
type: "
|
|
140
|
+
type: "response",
|
|
141
141
|
body: `PatchRelay is preparing the ${desiredStage} workflow from your latest prompt.`,
|
|
142
142
|
}, { ephemeral: false });
|
|
143
143
|
return;
|