patchrelay 0.7.4 → 0.7.6
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-session-plan.js +34 -0
- package/dist/agent-session-presentation.js +22 -0
- package/dist/build-info.json +3 -3
- package/dist/http.js +180 -0
- package/dist/issue-query-service.js +15 -0
- package/dist/linear-client.js +26 -0
- package/dist/public-agent-session-status.js +77 -0
- package/dist/service-queue.js +7 -2
- package/dist/service-runtime.js +2 -2
- package/dist/service-stage-runner.js +4 -2
- package/dist/service-webhook-processor.js +1 -1
- package/dist/service.js +42 -1
- package/dist/stage-agent-activity-publisher.js +27 -1
- package/dist/stage-failure.js +37 -23
- package/dist/stage-lifecycle-publisher.js +67 -25
- package/dist/webhook-agent-session-handler.js +17 -4
- package/dist/webhook-desired-stage-recorder.js +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function formatStageLabel(stage) {
|
|
2
|
+
return stage.replace(/[-_]+/g, " ");
|
|
3
|
+
}
|
|
4
|
+
function titleCase(value) {
|
|
5
|
+
return value
|
|
6
|
+
.split(/\s+/)
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.map((word) => word[0].toUpperCase() + word.slice(1))
|
|
9
|
+
.join(" ");
|
|
10
|
+
}
|
|
11
|
+
function buildPlan(stage, statuses) {
|
|
12
|
+
const stageLabel = titleCase(formatStageLabel(stage));
|
|
13
|
+
return [
|
|
14
|
+
{ label: "Prepare workspace", status: statuses[0] },
|
|
15
|
+
{ label: `Run ${stageLabel} workflow`, status: statuses[1] },
|
|
16
|
+
{ label: "Review next Linear step", status: statuses[2] },
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
export function buildPreparingSessionPlan(stage) {
|
|
20
|
+
return buildPlan(stage, ["in_progress", "pending", "pending"]);
|
|
21
|
+
}
|
|
22
|
+
export function buildRunningSessionPlan(stage) {
|
|
23
|
+
return buildPlan(stage, ["completed", "in_progress", "pending"]);
|
|
24
|
+
}
|
|
25
|
+
export function buildCompletedSessionPlan(stage) {
|
|
26
|
+
return buildPlan(stage, ["completed", "completed", "completed"]);
|
|
27
|
+
}
|
|
28
|
+
export function buildAwaitingHandoffSessionPlan(stage) {
|
|
29
|
+
return buildPlan(stage, ["completed", "completed", "in_progress"]);
|
|
30
|
+
}
|
|
31
|
+
export function buildFailedSessionPlan(stage, stageRun) {
|
|
32
|
+
const workflowStepStatus = stageRun?.threadId || stageRun?.turnId ? "completed" : "in_progress";
|
|
33
|
+
return buildPlan(stage, ["completed", workflowStepStatus, "pending"]);
|
|
34
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { buildSessionStatusUrl, createSessionStatusToken, deriveSessionStatusSigningSecret, } from "./public-agent-session-status.js";
|
|
2
|
+
const SESSION_STATUS_TTL_SECONDS = 60 * 60 * 24 * 7;
|
|
3
|
+
export function buildAgentSessionExternalUrls(config, issueKey) {
|
|
4
|
+
if (!issueKey || !config.server.publicBaseUrl) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
const token = createSessionStatusToken({
|
|
8
|
+
issueKey,
|
|
9
|
+
secret: deriveSessionStatusSigningSecret(config.linear.tokenEncryptionKey),
|
|
10
|
+
ttlSeconds: SESSION_STATUS_TTL_SECONDS,
|
|
11
|
+
});
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
label: "PatchRelay status",
|
|
15
|
+
url: buildSessionStatusUrl({
|
|
16
|
+
publicBaseUrl: config.server.publicBaseUrl,
|
|
17
|
+
issueKey,
|
|
18
|
+
token: token.token,
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
}
|
package/dist/build-info.json
CHANGED
package/dist/http.js
CHANGED
|
@@ -177,6 +177,36 @@ export async function buildHttpServer(config, service, logger) {
|
|
|
177
177
|
});
|
|
178
178
|
return reply.code(result.status).send(result.body);
|
|
179
179
|
});
|
|
180
|
+
app.get("/agent/session/:issueKey", async (request, reply) => {
|
|
181
|
+
const issueKey = request.params.issueKey;
|
|
182
|
+
const token = getQueryParam(request, "token");
|
|
183
|
+
if (!token) {
|
|
184
|
+
return reply
|
|
185
|
+
.code(401)
|
|
186
|
+
.type("text/html; charset=utf-8")
|
|
187
|
+
.send(renderAgentSessionStatusErrorPage("Missing access token."));
|
|
188
|
+
}
|
|
189
|
+
const status = await service.getPublicAgentSessionStatus({ issueKey, token });
|
|
190
|
+
if (status.status === "invalid_token") {
|
|
191
|
+
return reply
|
|
192
|
+
.code(401)
|
|
193
|
+
.type("text/html; charset=utf-8")
|
|
194
|
+
.send(renderAgentSessionStatusErrorPage("The access token is invalid or expired."));
|
|
195
|
+
}
|
|
196
|
+
if (status.status === "issue_not_found") {
|
|
197
|
+
return reply
|
|
198
|
+
.code(404)
|
|
199
|
+
.type("text/html; charset=utf-8")
|
|
200
|
+
.send(renderAgentSessionStatusErrorPage("Issue status is not available."));
|
|
201
|
+
}
|
|
202
|
+
return reply
|
|
203
|
+
.type("text/html; charset=utf-8")
|
|
204
|
+
.send(renderAgentSessionStatusPage({
|
|
205
|
+
issueKey,
|
|
206
|
+
expiresAt: status.expiresAt,
|
|
207
|
+
sessionStatus: status.sessionStatus,
|
|
208
|
+
}));
|
|
209
|
+
});
|
|
180
210
|
if (config.operatorApi.enabled) {
|
|
181
211
|
app.addHook("onRequest", async (request, reply) => {
|
|
182
212
|
if (!request.url.startsWith("/api/")) {
|
|
@@ -218,6 +248,19 @@ export async function buildHttpServer(config, service, logger) {
|
|
|
218
248
|
}
|
|
219
249
|
return reply.send({ ok: true, ...result });
|
|
220
250
|
});
|
|
251
|
+
app.get("/api/issues/:issueKey/session-url", async (request, reply) => {
|
|
252
|
+
const issueKey = request.params.issueKey;
|
|
253
|
+
const ttlSeconds = getPositiveIntegerQueryParam(request, "ttlSeconds");
|
|
254
|
+
const issue = await service.getIssueOverview(issueKey);
|
|
255
|
+
if (!issue) {
|
|
256
|
+
return reply.code(404).send({ ok: false, reason: "issue_not_found" });
|
|
257
|
+
}
|
|
258
|
+
const link = service.createPublicAgentSessionStatusLink(issueKey, ttlSeconds ? { ttlSeconds } : undefined);
|
|
259
|
+
if (!link) {
|
|
260
|
+
return reply.code(503).send({ ok: false, reason: "public_base_url_not_configured" });
|
|
261
|
+
}
|
|
262
|
+
return reply.send({ ok: true, ...link });
|
|
263
|
+
});
|
|
221
264
|
}
|
|
222
265
|
if (managementRoutesEnabled) {
|
|
223
266
|
app.get("/api/feed", async (request, reply) => {
|
|
@@ -371,3 +414,140 @@ function renderOAuthResult(message) {
|
|
|
371
414
|
</body>
|
|
372
415
|
</html>`;
|
|
373
416
|
}
|
|
417
|
+
function renderAgentSessionStatusErrorPage(message) {
|
|
418
|
+
return `<!doctype html>
|
|
419
|
+
<html lang="en">
|
|
420
|
+
<head>
|
|
421
|
+
<meta charset="utf-8">
|
|
422
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
423
|
+
<title>PatchRelay Agent Status</title>
|
|
424
|
+
<style>
|
|
425
|
+
body { font-family: Georgia, "Times New Roman", serif; background: #f7f4ef; color: #1f1d1a; margin: 0; padding: 32px; }
|
|
426
|
+
main { max-width: 720px; margin: 10vh auto; background: rgba(255,255,255,0.86); border: 1px solid rgba(31,29,26,0.12); border-radius: 20px; padding: 32px; box-shadow: 0 28px 80px rgba(49,42,30,0.10); }
|
|
427
|
+
p { font-size: 18px; line-height: 1.6; color: #4d483f; }
|
|
428
|
+
</style>
|
|
429
|
+
</head>
|
|
430
|
+
<body>
|
|
431
|
+
<main>
|
|
432
|
+
<h1>PatchRelay Agent Session</h1>
|
|
433
|
+
<p>${escapeHtml(message)}</p>
|
|
434
|
+
</main>
|
|
435
|
+
</body>
|
|
436
|
+
</html>`;
|
|
437
|
+
}
|
|
438
|
+
function renderAgentSessionStatusPage(params) {
|
|
439
|
+
const issueTitle = params.sessionStatus.issue.title ?? params.sessionStatus.issue.issueKey ?? params.issueKey;
|
|
440
|
+
const issueUrl = params.sessionStatus.issue.issueUrl;
|
|
441
|
+
const activeStage = formatStageChip(params.sessionStatus.activeStageRun);
|
|
442
|
+
const latestStage = formatStageChip(params.sessionStatus.latestStageRun);
|
|
443
|
+
const threadInfo = formatThread(params.sessionStatus.liveThread);
|
|
444
|
+
const stagesRows = params.sessionStatus.stages.slice(-8).map((entry) => formatStageRow(entry.stageRun)).join("");
|
|
445
|
+
return `<!doctype html>
|
|
446
|
+
<html lang="en">
|
|
447
|
+
<head>
|
|
448
|
+
<meta charset="utf-8">
|
|
449
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
450
|
+
<title>PatchRelay Agent Session ${escapeHtml(params.issueKey)}</title>
|
|
451
|
+
<style>
|
|
452
|
+
:root {
|
|
453
|
+
color-scheme: light;
|
|
454
|
+
--bg: #f5f1e8;
|
|
455
|
+
--panel: rgba(255,255,255,0.84);
|
|
456
|
+
--ink: #1f1d1a;
|
|
457
|
+
--muted: #5b554b;
|
|
458
|
+
--accent: #1f6d57;
|
|
459
|
+
--line: rgba(31,29,26,0.15);
|
|
460
|
+
}
|
|
461
|
+
* { box-sizing: border-box; }
|
|
462
|
+
body {
|
|
463
|
+
margin: 0;
|
|
464
|
+
min-height: 100vh;
|
|
465
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
466
|
+
color: var(--ink);
|
|
467
|
+
background:
|
|
468
|
+
radial-gradient(circle at top left, rgba(184, 139, 68, 0.22), transparent 36%),
|
|
469
|
+
radial-gradient(circle at bottom right, rgba(31, 109, 87, 0.16), transparent 30%),
|
|
470
|
+
linear-gradient(150deg, #eee3cf 0%, var(--bg) 52%, #f8f6f1 100%);
|
|
471
|
+
padding: 24px;
|
|
472
|
+
}
|
|
473
|
+
main {
|
|
474
|
+
width: min(920px, 100%);
|
|
475
|
+
margin: 0 auto;
|
|
476
|
+
background: var(--panel);
|
|
477
|
+
border: 1px solid var(--line);
|
|
478
|
+
border-radius: 22px;
|
|
479
|
+
padding: 32px;
|
|
480
|
+
box-shadow: 0 30px 86px rgba(49,42,30,0.12);
|
|
481
|
+
}
|
|
482
|
+
h1 { margin: 0; font-size: clamp(34px, 7vw, 52px); line-height: 1.05; }
|
|
483
|
+
p { color: var(--muted); font-size: 17px; line-height: 1.6; margin: 12px 0 0; }
|
|
484
|
+
a { color: var(--accent); text-decoration: none; }
|
|
485
|
+
a:hover { text-decoration: underline; }
|
|
486
|
+
.chips { display: flex; flex-wrap: wrap; gap: 10px; margin: 20px 0 6px; }
|
|
487
|
+
.chip { border: 1px solid var(--line); border-radius: 999px; padding: 9px 14px; background: rgba(255,255,255,0.74); font-size: 14px; }
|
|
488
|
+
.section { margin-top: 24px; padding-top: 18px; border-top: 1px solid var(--line); }
|
|
489
|
+
.section h2 { margin: 0; font-size: 22px; }
|
|
490
|
+
table { width: 100%; border-collapse: collapse; margin-top: 14px; }
|
|
491
|
+
th, td { text-align: left; border-bottom: 1px solid var(--line); padding: 10px 8px; vertical-align: top; }
|
|
492
|
+
th { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: #5f594e; }
|
|
493
|
+
td { font-size: 15px; color: #2a2622; }
|
|
494
|
+
code { font-family: "SFMono-Regular", "Cascadia Code", "Fira Code", monospace; font-size: 0.95em; }
|
|
495
|
+
</style>
|
|
496
|
+
</head>
|
|
497
|
+
<body>
|
|
498
|
+
<main>
|
|
499
|
+
<h1>${escapeHtml(issueTitle)}</h1>
|
|
500
|
+
<p>PatchRelay read-only agent session status for <code>${escapeHtml(params.issueKey)}</code>.</p>
|
|
501
|
+
${issueUrl ? `<p><a href="${escapeHtml(issueUrl)}" target="_blank" rel="noopener noreferrer">Open issue in Linear</a></p>` : ""}
|
|
502
|
+
<div class="chips">
|
|
503
|
+
<span class="chip"><strong>Active:</strong> ${activeStage}</span>
|
|
504
|
+
<span class="chip"><strong>Latest:</strong> ${latestStage}</span>
|
|
505
|
+
<span class="chip"><strong>Thread:</strong> ${threadInfo}</span>
|
|
506
|
+
</div>
|
|
507
|
+
<div class="section">
|
|
508
|
+
<h2>Recent Stages</h2>
|
|
509
|
+
<table>
|
|
510
|
+
<thead>
|
|
511
|
+
<tr>
|
|
512
|
+
<th>Stage</th>
|
|
513
|
+
<th>Status</th>
|
|
514
|
+
<th>Started</th>
|
|
515
|
+
<th>Ended</th>
|
|
516
|
+
</tr>
|
|
517
|
+
</thead>
|
|
518
|
+
<tbody>
|
|
519
|
+
${stagesRows || '<tr><td colspan="4">No completed stage runs yet.</td></tr>'}
|
|
520
|
+
</tbody>
|
|
521
|
+
</table>
|
|
522
|
+
</div>
|
|
523
|
+
<p>Snapshot generated at <code>${escapeHtml(params.sessionStatus.generatedAt)}</code>. Link valid until <code>${escapeHtml(params.expiresAt)}</code>.</p>
|
|
524
|
+
</main>
|
|
525
|
+
</body>
|
|
526
|
+
</html>`;
|
|
527
|
+
}
|
|
528
|
+
function formatStageChip(stageRun) {
|
|
529
|
+
if (!stageRun) {
|
|
530
|
+
return "none";
|
|
531
|
+
}
|
|
532
|
+
const stage = stageRun.stage ?? "unknown";
|
|
533
|
+
const status = stageRun.status ?? "unknown";
|
|
534
|
+
return `<code>${escapeHtml(stage)}</code> (${escapeHtml(status)})`;
|
|
535
|
+
}
|
|
536
|
+
function formatThread(liveThread) {
|
|
537
|
+
if (!liveThread) {
|
|
538
|
+
return "idle";
|
|
539
|
+
}
|
|
540
|
+
const threadId = liveThread.threadId ?? "unknown";
|
|
541
|
+
const status = liveThread.threadStatus ?? "unknown";
|
|
542
|
+
return `<code>${escapeHtml(threadId)}</code> (${escapeHtml(status)})`;
|
|
543
|
+
}
|
|
544
|
+
function formatStageRow(stageRun) {
|
|
545
|
+
if (!stageRun) {
|
|
546
|
+
return '<tr><td colspan="4">Unknown stage record</td></tr>';
|
|
547
|
+
}
|
|
548
|
+
const stage = stageRun.stage ?? "unknown";
|
|
549
|
+
const status = stageRun.status ?? "unknown";
|
|
550
|
+
const startedAt = stageRun.startedAt ?? "-";
|
|
551
|
+
const endedAt = stageRun.endedAt ?? "-";
|
|
552
|
+
return `<tr><td><code>${escapeHtml(stage)}</code></td><td>${escapeHtml(status)}</td><td><code>${escapeHtml(startedAt)}</code></td><td><code>${escapeHtml(endedAt)}</code></td></tr>`;
|
|
553
|
+
}
|
|
@@ -65,4 +65,19 @@ export class IssueQueryService {
|
|
|
65
65
|
async getActiveStageStatus(issueKey) {
|
|
66
66
|
return await this.stageFinalizer.getActiveStageStatus(issueKey);
|
|
67
67
|
}
|
|
68
|
+
async getPublicAgentSessionStatus(issueKey) {
|
|
69
|
+
const overview = await this.getIssueOverview(issueKey);
|
|
70
|
+
if (!overview) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const report = await this.getIssueReport(issueKey);
|
|
74
|
+
return {
|
|
75
|
+
issue: overview.issue,
|
|
76
|
+
...(overview.activeStageRun ? { activeStageRun: overview.activeStageRun } : {}),
|
|
77
|
+
...(overview.latestStageRun ? { latestStageRun: overview.latestStageRun } : {}),
|
|
78
|
+
...(overview.liveThread ? { liveThread: overview.liveThread } : {}),
|
|
79
|
+
stages: report?.stages ?? [],
|
|
80
|
+
generatedAt: new Date().toISOString(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
68
83
|
}
|
package/dist/linear-client.js
CHANGED
|
@@ -156,6 +156,32 @@ export class LinearGraphqlClient {
|
|
|
156
156
|
}
|
|
157
157
|
return response.agentActivityCreate.agentActivity;
|
|
158
158
|
}
|
|
159
|
+
async updateAgentSession(params) {
|
|
160
|
+
const input = {};
|
|
161
|
+
if ("externalUrls" in params) {
|
|
162
|
+
input.externalUrls = params.externalUrls;
|
|
163
|
+
}
|
|
164
|
+
if ("plan" in params) {
|
|
165
|
+
input.plan = params.plan;
|
|
166
|
+
}
|
|
167
|
+
const response = await this.request(`
|
|
168
|
+
mutation PatchRelayUpdateAgentSession($id: String!, $input: AgentSessionUpdateInput!) {
|
|
169
|
+
agentSessionUpdate(id: $id, input: $input) {
|
|
170
|
+
success
|
|
171
|
+
agentSession {
|
|
172
|
+
id
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
`, {
|
|
177
|
+
id: params.agentSessionId,
|
|
178
|
+
input,
|
|
179
|
+
});
|
|
180
|
+
if (!response.agentSessionUpdate.success || !response.agentSessionUpdate.agentSession) {
|
|
181
|
+
throw new Error(`Linear rejected agent session update for session ${params.agentSessionId}`);
|
|
182
|
+
}
|
|
183
|
+
return response.agentSessionUpdate.agentSession;
|
|
184
|
+
}
|
|
159
185
|
async updateIssueLabels(params) {
|
|
160
186
|
const issue = await this.getIssue(params.issueId);
|
|
161
187
|
const addIds = this.resolveLabelIds(issue, params.addNames ?? []);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
const TOKEN_VERSION = 1;
|
|
3
|
+
const DEFAULT_TTL_SECONDS = 60 * 60 * 24;
|
|
4
|
+
const PURPOSE = "patchrelay-agent-session-status";
|
|
5
|
+
export function createSessionStatusToken(params) {
|
|
6
|
+
const nowSeconds = Math.floor((params.nowMs ?? Date.now()) / 1000);
|
|
7
|
+
const ttlSeconds = Number.isFinite(params.ttlSeconds) ? Math.max(60, Math.floor(params.ttlSeconds ?? DEFAULT_TTL_SECONDS)) : DEFAULT_TTL_SECONDS;
|
|
8
|
+
const payload = {
|
|
9
|
+
v: TOKEN_VERSION,
|
|
10
|
+
i: params.issueKey,
|
|
11
|
+
exp: nowSeconds + ttlSeconds,
|
|
12
|
+
};
|
|
13
|
+
const payloadEncoded = encodeBase64Url(Buffer.from(JSON.stringify(payload), "utf8"));
|
|
14
|
+
const signature = signPayload(payloadEncoded, params.secret);
|
|
15
|
+
return {
|
|
16
|
+
token: `${payloadEncoded}.${signature}`,
|
|
17
|
+
issueKey: payload.i,
|
|
18
|
+
expiresAt: new Date(payload.exp * 1000).toISOString(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function verifySessionStatusToken(token, secret, nowMs) {
|
|
22
|
+
const [payloadEncoded, signatureEncoded] = token.split(".", 2);
|
|
23
|
+
if (!payloadEncoded || !signatureEncoded) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const expectedSignature = signPayload(payloadEncoded, secret);
|
|
27
|
+
if (!timingSafeEqualUtf8(signatureEncoded, expectedSignature)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const payload = parsePayload(payloadEncoded);
|
|
31
|
+
if (!payload || payload.v !== TOKEN_VERSION || typeof payload.i !== "string" || typeof payload.exp !== "number") {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const nowSeconds = Math.floor((nowMs ?? Date.now()) / 1000);
|
|
35
|
+
if (payload.exp < nowSeconds) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
issueKey: payload.i,
|
|
40
|
+
expiresAt: new Date(payload.exp * 1000).toISOString(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function buildSessionStatusUrl(params) {
|
|
44
|
+
const url = new URL(`/agent/session/${encodeURIComponent(params.issueKey)}`, params.publicBaseUrl);
|
|
45
|
+
url.searchParams.set("token", params.token);
|
|
46
|
+
return url.toString();
|
|
47
|
+
}
|
|
48
|
+
export function deriveSessionStatusSigningSecret(tokenEncryptionKey) {
|
|
49
|
+
return `${PURPOSE}:${tokenEncryptionKey}`;
|
|
50
|
+
}
|
|
51
|
+
function signPayload(payloadEncoded, secret) {
|
|
52
|
+
const digest = createHmac("sha256", secret).update(payloadEncoded).digest();
|
|
53
|
+
return encodeBase64Url(digest);
|
|
54
|
+
}
|
|
55
|
+
function parsePayload(payloadEncoded) {
|
|
56
|
+
try {
|
|
57
|
+
const decoded = decodeBase64Url(payloadEncoded).toString("utf8");
|
|
58
|
+
return JSON.parse(decoded);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function encodeBase64Url(value) {
|
|
65
|
+
return value.toString("base64url");
|
|
66
|
+
}
|
|
67
|
+
function decodeBase64Url(value) {
|
|
68
|
+
return Buffer.from(value, "base64url");
|
|
69
|
+
}
|
|
70
|
+
function timingSafeEqualUtf8(left, right) {
|
|
71
|
+
const leftBuffer = Buffer.from(left, "utf8");
|
|
72
|
+
const rightBuffer = Buffer.from(right, "utf8");
|
|
73
|
+
if (leftBuffer.length !== rightBuffer.length) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
77
|
+
}
|
package/dist/service-queue.js
CHANGED
|
@@ -10,12 +10,17 @@ export class SerialWorkQueue {
|
|
|
10
10
|
this.logger = logger;
|
|
11
11
|
this.getKey = getKey;
|
|
12
12
|
}
|
|
13
|
-
enqueue(item) {
|
|
13
|
+
enqueue(item, options) {
|
|
14
14
|
const key = this.getKey?.(item);
|
|
15
15
|
if (key && this.queuedKeys.has(key)) {
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
if (options?.priority) {
|
|
19
|
+
this.items.unshift(item);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
this.items.push(item);
|
|
23
|
+
}
|
|
19
24
|
if (key) {
|
|
20
25
|
this.queuedKeys.add(key);
|
|
21
26
|
}
|
package/dist/service-runtime.js
CHANGED
|
@@ -80,8 +80,8 @@ export class ServiceRuntime {
|
|
|
80
80
|
this.ready = false;
|
|
81
81
|
void this.codex.stop();
|
|
82
82
|
}
|
|
83
|
-
enqueueWebhookEvent(eventId) {
|
|
84
|
-
this.webhookQueue.enqueue(eventId);
|
|
83
|
+
enqueueWebhookEvent(eventId, options) {
|
|
84
|
+
this.webhookQueue.enqueue(eventId, options);
|
|
85
85
|
}
|
|
86
86
|
enqueueIssue(projectId, issueId) {
|
|
87
87
|
this.issueQueue.enqueue({ projectId, issueId });
|
|
@@ -133,8 +133,10 @@ export class ServiceStageRunner {
|
|
|
133
133
|
failureMessage: "Failed to deliver queued Linear comment during stage startup",
|
|
134
134
|
...(claim.issue.issueKey ? { issueKey: claim.issue.issueKey } : {}),
|
|
135
135
|
});
|
|
136
|
-
await this.lifecyclePublisher.
|
|
137
|
-
|
|
136
|
+
const deliveredToSession = await this.lifecyclePublisher.publishStageStarted(claim.issue, claim.stageRun.stage);
|
|
137
|
+
if (!deliveredToSession) {
|
|
138
|
+
await this.lifecyclePublisher.refreshRunningStatusComment(item.projectId, item.issueId, claim.stageRun.id, issue.issueKey);
|
|
139
|
+
}
|
|
138
140
|
this.logger.info({
|
|
139
141
|
issueKey: issue.issueKey,
|
|
140
142
|
stage: claim.stageRun.stage,
|
|
@@ -24,7 +24,7 @@ export class ServiceWebhookProcessor {
|
|
|
24
24
|
this.logger = logger;
|
|
25
25
|
this.feed = feed;
|
|
26
26
|
const turnInputDispatcher = new StageTurnInputDispatcher(stores, codex, logger);
|
|
27
|
-
const agentActivity = new StageAgentActivityPublisher(linearProvider, logger);
|
|
27
|
+
const agentActivity = new StageAgentActivityPublisher(config, linearProvider, logger);
|
|
28
28
|
this.desiredStageRecorder = new WebhookDesiredStageRecorder(stores);
|
|
29
29
|
this.agentSessionHandler = new AgentSessionWebhookHandler(stores, turnInputDispatcher, agentActivity, feed);
|
|
30
30
|
this.commentHandler = new CommentWebhookHandler(stores, turnInputDispatcher, feed);
|
package/dist/service.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { IssueQueryService } from "./issue-query-service.js";
|
|
2
2
|
import { LinearOAuthService } from "./linear-oauth-service.js";
|
|
3
3
|
import { OperatorEventFeed } from "./operator-feed.js";
|
|
4
|
+
import { buildSessionStatusUrl, createSessionStatusToken, deriveSessionStatusSigningSecret, verifySessionStatusToken, } from "./public-agent-session-status.js";
|
|
4
5
|
import { ServiceRuntime } from "./service-runtime.js";
|
|
5
6
|
import { ServiceStageFinalizer } from "./service-stage-finalizer.js";
|
|
6
7
|
import { ServiceStageRunner } from "./service-stage-runner.js";
|
|
@@ -111,7 +112,9 @@ export class PatchRelayService {
|
|
|
111
112
|
rawBody: params.rawBody,
|
|
112
113
|
});
|
|
113
114
|
if (result.accepted) {
|
|
114
|
-
this.runtime.enqueueWebhookEvent(result.accepted.id
|
|
115
|
+
this.runtime.enqueueWebhookEvent(result.accepted.id, {
|
|
116
|
+
priority: result.accepted.normalized.triggerEvent === "agentSessionCreated",
|
|
117
|
+
});
|
|
115
118
|
}
|
|
116
119
|
return {
|
|
117
120
|
status: result.status,
|
|
@@ -136,6 +139,44 @@ export class PatchRelayService {
|
|
|
136
139
|
async getActiveStageStatus(issueKey) {
|
|
137
140
|
return await this.queryService.getActiveStageStatus(issueKey);
|
|
138
141
|
}
|
|
142
|
+
createPublicAgentSessionStatusLink(issueKey, options) {
|
|
143
|
+
if (!this.config.server.publicBaseUrl) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const signingSecret = deriveSessionStatusSigningSecret(this.config.linear.tokenEncryptionKey);
|
|
147
|
+
const token = createSessionStatusToken({
|
|
148
|
+
issueKey,
|
|
149
|
+
secret: signingSecret,
|
|
150
|
+
...(options?.nowMs !== undefined ? { nowMs: options.nowMs } : {}),
|
|
151
|
+
...(options?.ttlSeconds !== undefined ? { ttlSeconds: options.ttlSeconds } : {}),
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
url: buildSessionStatusUrl({
|
|
155
|
+
publicBaseUrl: this.config.server.publicBaseUrl,
|
|
156
|
+
issueKey,
|
|
157
|
+
token: token.token,
|
|
158
|
+
}),
|
|
159
|
+
issueKey: token.issueKey,
|
|
160
|
+
expiresAt: token.expiresAt,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async getPublicAgentSessionStatus(params) {
|
|
164
|
+
const signingSecret = deriveSessionStatusSigningSecret(this.config.linear.tokenEncryptionKey);
|
|
165
|
+
const parsed = verifySessionStatusToken(params.token, signingSecret, params.nowMs);
|
|
166
|
+
if (!parsed || parsed.issueKey.trim().toLowerCase() !== params.issueKey.trim().toLowerCase()) {
|
|
167
|
+
return { status: "invalid_token" };
|
|
168
|
+
}
|
|
169
|
+
const sessionStatus = await this.queryService.getPublicAgentSessionStatus(params.issueKey);
|
|
170
|
+
if (!sessionStatus) {
|
|
171
|
+
return { status: "issue_not_found" };
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
status: "ok",
|
|
175
|
+
issueKey: params.issueKey,
|
|
176
|
+
expiresAt: parsed.expiresAt,
|
|
177
|
+
sessionStatus,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
139
180
|
}
|
|
140
181
|
function toLinearClientProvider(linear) {
|
|
141
182
|
if (linear && typeof linear.forProject === "function") {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
|
|
1
2
|
export class StageAgentActivityPublisher {
|
|
3
|
+
config;
|
|
2
4
|
linearProvider;
|
|
3
5
|
logger;
|
|
4
|
-
constructor(linearProvider, logger) {
|
|
6
|
+
constructor(config, linearProvider, logger) {
|
|
7
|
+
this.config = config;
|
|
5
8
|
this.linearProvider = linearProvider;
|
|
6
9
|
this.logger = logger;
|
|
7
10
|
}
|
|
@@ -30,4 +33,27 @@ export class StageAgentActivityPublisher {
|
|
|
30
33
|
}
|
|
31
34
|
await this.publishForSession(issue.projectId, issue.activeAgentSessionId, content);
|
|
32
35
|
}
|
|
36
|
+
async updateSession(params) {
|
|
37
|
+
const linear = await this.linearProvider.forProject(params.projectId);
|
|
38
|
+
if (!linear?.updateAgentSession) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const externalUrls = buildAgentSessionExternalUrls(this.config, params.issueKey);
|
|
42
|
+
if (!externalUrls && !params.plan) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await linear.updateAgentSession({
|
|
47
|
+
agentSessionId: params.agentSessionId,
|
|
48
|
+
...(externalUrls ? { externalUrls } : {}),
|
|
49
|
+
...(params.plan ? { plan: params.plan } : {}),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
this.logger.warn({
|
|
54
|
+
agentSessionId: params.agentSessionId,
|
|
55
|
+
error: error instanceof Error ? error.message : String(error),
|
|
56
|
+
}, "Failed to update Linear agent session");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
33
59
|
}
|
package/dist/stage-failure.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildFailedSessionPlan } from "./agent-session-plan.js";
|
|
1
2
|
import { buildStageFailedComment, resolveActiveLinearState, resolveFallbackLinearState, resolveWorkflowLabelCleanup, } from "./linear-workflow.js";
|
|
2
3
|
function normalizeStateName(value) {
|
|
3
4
|
const trimmed = value?.trim();
|
|
@@ -48,31 +49,44 @@ export async function syncFailedStageToLinear(params) {
|
|
|
48
49
|
lifecycleStatus: "failed",
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
.upsertIssueComment({
|
|
53
|
-
issueId: params.stageRun.linearIssueId,
|
|
54
|
-
...(params.issue.statusCommentId ? { commentId: params.issue.statusCommentId } : {}),
|
|
55
|
-
body: buildStageFailedComment({
|
|
56
|
-
issue: params.issue,
|
|
57
|
-
stageRun: params.stageRun,
|
|
58
|
-
message: params.message,
|
|
59
|
-
...(fallbackState ? { fallbackState } : {}),
|
|
60
|
-
...(params.mode ? { mode: params.mode } : {}),
|
|
61
|
-
}),
|
|
62
|
-
})
|
|
63
|
-
.catch(() => undefined);
|
|
64
|
-
if (result) {
|
|
65
|
-
params.stores.workflowCoordinator.setIssueStatusComment(params.stageRun.projectId, params.stageRun.linearIssueId, result.id);
|
|
66
|
-
}
|
|
52
|
+
let deliveredToSession = false;
|
|
67
53
|
if (params.issue.activeAgentSessionId) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
54
|
+
deliveredToSession =
|
|
55
|
+
(await linear
|
|
56
|
+
.updateAgentSession?.({
|
|
57
|
+
agentSessionId: params.issue.activeAgentSessionId,
|
|
58
|
+
plan: buildFailedSessionPlan(params.stageRun.stage, params.stageRun),
|
|
59
|
+
})
|
|
60
|
+
.then(() => true)
|
|
61
|
+
.catch(() => false)) ?? false;
|
|
62
|
+
deliveredToSession =
|
|
63
|
+
(await linear
|
|
64
|
+
.createAgentActivity({
|
|
65
|
+
agentSessionId: params.issue.activeAgentSessionId,
|
|
66
|
+
content: {
|
|
67
|
+
type: "error",
|
|
68
|
+
body: `PatchRelay could not complete the ${params.stageRun.stage} workflow: ${params.message}`,
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
.then(() => true)
|
|
72
|
+
.catch(() => false)) || deliveredToSession;
|
|
73
|
+
}
|
|
74
|
+
if (!deliveredToSession) {
|
|
75
|
+
const result = await linear
|
|
76
|
+
.upsertIssueComment({
|
|
77
|
+
issueId: params.stageRun.linearIssueId,
|
|
78
|
+
...(params.issue.statusCommentId ? { commentId: params.issue.statusCommentId } : {}),
|
|
79
|
+
body: buildStageFailedComment({
|
|
80
|
+
issue: params.issue,
|
|
81
|
+
stageRun: params.stageRun,
|
|
82
|
+
message: params.message,
|
|
83
|
+
...(fallbackState ? { fallbackState } : {}),
|
|
84
|
+
...(params.mode ? { mode: params.mode } : {}),
|
|
85
|
+
}),
|
|
75
86
|
})
|
|
76
87
|
.catch(() => undefined);
|
|
88
|
+
if (result) {
|
|
89
|
+
params.stores.workflowCoordinator.setIssueStatusComment(params.stageRun.projectId, params.stageRun.linearIssueId, result.id);
|
|
90
|
+
}
|
|
77
91
|
}
|
|
78
92
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buildAwaitingHandoffSessionPlan, buildCompletedSessionPlan, buildRunningSessionPlan, } from "./agent-session-plan.js";
|
|
2
|
+
import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
|
|
1
3
|
import { buildAwaitingHandoffComment, buildRunningStatusComment, resolveActiveLinearState, resolveWorkflowLabelCleanup, resolveWorkflowLabelNames, } from "./linear-workflow.js";
|
|
2
4
|
import { sanitizeDiagnosticText } from "./utils.js";
|
|
3
5
|
export class StageLifecyclePublisher {
|
|
@@ -71,12 +73,13 @@ export class StageLifecyclePublisher {
|
|
|
71
73
|
}
|
|
72
74
|
async publishStageStarted(issue, stage) {
|
|
73
75
|
if (!issue.activeAgentSessionId) {
|
|
74
|
-
return;
|
|
76
|
+
return false;
|
|
75
77
|
}
|
|
76
78
|
const linear = await this.linearProvider.forProject(issue.projectId);
|
|
77
79
|
if (!linear) {
|
|
78
|
-
return;
|
|
80
|
+
return false;
|
|
79
81
|
}
|
|
82
|
+
const sessionUpdated = await this.updateAgentSession(linear, issue, buildRunningSessionPlan(stage), stage);
|
|
80
83
|
try {
|
|
81
84
|
await linear.createAgentActivity({
|
|
82
85
|
agentSessionId: issue.activeAgentSessionId,
|
|
@@ -88,6 +91,7 @@ export class StageLifecyclePublisher {
|
|
|
88
91
|
},
|
|
89
92
|
ephemeral: true,
|
|
90
93
|
});
|
|
94
|
+
return true;
|
|
91
95
|
}
|
|
92
96
|
catch (error) {
|
|
93
97
|
this.logger.warn({
|
|
@@ -96,6 +100,7 @@ export class StageLifecyclePublisher {
|
|
|
96
100
|
agentSessionId: issue.activeAgentSessionId,
|
|
97
101
|
error: error instanceof Error ? error.message : String(error),
|
|
98
102
|
}, "Failed to publish Linear agent activity after stage startup");
|
|
103
|
+
return sessionUpdated;
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
async publishStageCompletion(stageRun, enqueueIssue) {
|
|
@@ -134,16 +139,10 @@ export class StageLifecyclePublisher {
|
|
|
134
139
|
}
|
|
135
140
|
this.stores.workflowCoordinator.setIssueLifecycleStatus(stageRun.projectId, stageRun.linearIssueId, "paused");
|
|
136
141
|
const finalStageRun = this.stores.issueWorkflows.getStageRun(stageRun.id) ?? stageRun;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
issue: refreshedIssue,
|
|
142
|
-
stageRun: finalStageRun,
|
|
143
|
-
activeState,
|
|
144
|
-
}),
|
|
145
|
-
});
|
|
146
|
-
this.stores.workflowCoordinator.setIssueStatusComment(stageRun.projectId, stageRun.linearIssueId, result.id);
|
|
142
|
+
let deliveredToSession = false;
|
|
143
|
+
if (refreshedIssue.activeAgentSessionId) {
|
|
144
|
+
deliveredToSession = await this.updateAgentSession(linear, refreshedIssue, buildAwaitingHandoffSessionPlan(stageRun.stage));
|
|
145
|
+
}
|
|
147
146
|
this.feed?.publish({
|
|
148
147
|
level: "info",
|
|
149
148
|
kind: "stage",
|
|
@@ -154,10 +153,23 @@ export class StageLifecyclePublisher {
|
|
|
154
153
|
summary: `Completed ${stageRun.stage} workflow`,
|
|
155
154
|
detail: `Waiting for a Linear state change or follow-up input while the issue remains in ${activeState}.`,
|
|
156
155
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
deliveredToSession =
|
|
157
|
+
(await this.publishAgentCompletion(refreshedIssue, {
|
|
158
|
+
type: "elicitation",
|
|
159
|
+
body: `PatchRelay finished the ${stageRun.stage} workflow. Move the issue to its next workflow state or leave a follow-up prompt to continue.`,
|
|
160
|
+
})) || deliveredToSession;
|
|
161
|
+
if (!deliveredToSession) {
|
|
162
|
+
const result = await linear.upsertIssueComment({
|
|
163
|
+
issueId: stageRun.linearIssueId,
|
|
164
|
+
...(refreshedIssue.statusCommentId ? { commentId: refreshedIssue.statusCommentId } : {}),
|
|
165
|
+
body: buildAwaitingHandoffComment({
|
|
166
|
+
issue: refreshedIssue,
|
|
167
|
+
stageRun: finalStageRun,
|
|
168
|
+
activeState,
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
this.stores.workflowCoordinator.setIssueStatusComment(stageRun.projectId, stageRun.linearIssueId, result.id);
|
|
172
|
+
}
|
|
161
173
|
return;
|
|
162
174
|
}
|
|
163
175
|
const cleanup = resolveWorkflowLabelCleanup(project);
|
|
@@ -179,6 +191,9 @@ export class StageLifecyclePublisher {
|
|
|
179
191
|
}
|
|
180
192
|
}
|
|
181
193
|
if (refreshedIssue) {
|
|
194
|
+
if (refreshedIssue.activeAgentSessionId && linear) {
|
|
195
|
+
await this.updateAgentSession(linear, refreshedIssue, buildCompletedSessionPlan(stageRun.stage));
|
|
196
|
+
}
|
|
182
197
|
this.feed?.publish({
|
|
183
198
|
level: "info",
|
|
184
199
|
kind: "stage",
|
|
@@ -194,20 +209,46 @@ export class StageLifecyclePublisher {
|
|
|
194
209
|
});
|
|
195
210
|
}
|
|
196
211
|
}
|
|
212
|
+
async updateAgentSession(linear, issue, plan, stage) {
|
|
213
|
+
if (!issue.activeAgentSessionId) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const externalUrls = buildAgentSessionExternalUrls(this.config, issue.issueKey);
|
|
218
|
+
await linear.updateAgentSession?.({
|
|
219
|
+
agentSessionId: issue.activeAgentSessionId,
|
|
220
|
+
...(externalUrls ? { externalUrls } : {}),
|
|
221
|
+
plan,
|
|
222
|
+
});
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
this.logger.warn({
|
|
227
|
+
issueKey: issue.issueKey,
|
|
228
|
+
issueId: issue.linearIssueId,
|
|
229
|
+
...(stage ? { stage } : {}),
|
|
230
|
+
agentSessionId: issue.activeAgentSessionId,
|
|
231
|
+
error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
|
|
232
|
+
}, "Failed to update Linear agent session");
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
197
236
|
async publishAgentCompletion(issue, content) {
|
|
198
237
|
if (!issue.activeAgentSessionId) {
|
|
199
|
-
return;
|
|
238
|
+
return false;
|
|
200
239
|
}
|
|
201
240
|
const linear = await this.linearProvider.forProject(issue.projectId);
|
|
202
241
|
if (!linear) {
|
|
203
|
-
return;
|
|
242
|
+
return false;
|
|
204
243
|
}
|
|
205
|
-
|
|
206
|
-
.createAgentActivity({
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
244
|
+
try {
|
|
245
|
+
await linear.createAgentActivity({
|
|
246
|
+
agentSessionId: issue.activeAgentSessionId,
|
|
247
|
+
content,
|
|
248
|
+
});
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
211
252
|
this.logger.warn({
|
|
212
253
|
issueKey: issue.issueKey,
|
|
213
254
|
issueId: issue.linearIssueId,
|
|
@@ -215,6 +256,7 @@ export class StageLifecyclePublisher {
|
|
|
215
256
|
activityType: content.type,
|
|
216
257
|
error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
|
|
217
258
|
}, "Failed to publish Linear agent activity");
|
|
218
|
-
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
219
261
|
}
|
|
220
262
|
}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { buildPreparingSessionPlan, buildRunningSessionPlan } from "./agent-session-plan.js";
|
|
2
3
|
import { triggerEventAllowed } from "./project-resolution.js";
|
|
3
4
|
import { listRunnableStates, resolveWorkflowStage } from "./workflow-policy.js";
|
|
4
5
|
function trimPrompt(value) {
|
|
5
6
|
const trimmed = value?.trim();
|
|
6
7
|
return trimmed ? trimmed : undefined;
|
|
7
8
|
}
|
|
9
|
+
function buildSessionUpdateParams(projectId, agentSessionId, issueKey, plan) {
|
|
10
|
+
return {
|
|
11
|
+
projectId,
|
|
12
|
+
agentSessionId,
|
|
13
|
+
...(issueKey ? { issueKey } : {}),
|
|
14
|
+
plan,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
8
17
|
export class AgentSessionWebhookHandler {
|
|
9
18
|
stores;
|
|
10
19
|
turnInputDispatcher;
|
|
@@ -30,6 +39,7 @@ export class AgentSessionWebhookHandler {
|
|
|
30
39
|
if (normalized.triggerEvent === "agentSessionCreated") {
|
|
31
40
|
if (!delegatedToPatchRelay) {
|
|
32
41
|
if (activeStage) {
|
|
42
|
+
await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildRunningSessionPlan(activeStage)));
|
|
33
43
|
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
34
44
|
type: "thought",
|
|
35
45
|
body: `PatchRelay is already running the ${activeStage} workflow for this issue. Delegate it to PatchRelay if you want automation to own the workflow, or keep replying here to steer the active run.`,
|
|
@@ -54,15 +64,17 @@ export class AgentSessionWebhookHandler {
|
|
|
54
64
|
return;
|
|
55
65
|
}
|
|
56
66
|
if (desiredStage) {
|
|
67
|
+
await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildPreparingSessionPlan(desiredStage)));
|
|
57
68
|
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
58
|
-
type: "
|
|
59
|
-
body: `PatchRelay
|
|
69
|
+
type: "response",
|
|
70
|
+
body: `PatchRelay picked this up and is preparing the ${desiredStage} workflow.`,
|
|
60
71
|
});
|
|
61
72
|
return;
|
|
62
73
|
}
|
|
63
74
|
if (activeStage) {
|
|
75
|
+
await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildRunningSessionPlan(activeStage)));
|
|
64
76
|
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
65
|
-
type: "
|
|
77
|
+
type: "response",
|
|
66
78
|
body: `PatchRelay is already running the ${activeStage} workflow for this issue.`,
|
|
67
79
|
});
|
|
68
80
|
}
|
|
@@ -123,8 +135,9 @@ export class AgentSessionWebhookHandler {
|
|
|
123
135
|
return;
|
|
124
136
|
}
|
|
125
137
|
if (!activeRunLease && desiredStage) {
|
|
138
|
+
await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildPreparingSessionPlan(desiredStage)));
|
|
126
139
|
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
127
|
-
type: "
|
|
140
|
+
type: "response",
|
|
128
141
|
body: `PatchRelay received your prompt and is preparing the ${desiredStage} workflow.`,
|
|
129
142
|
});
|
|
130
143
|
return;
|
|
@@ -27,6 +27,8 @@ export class WebhookDesiredStageRecorder {
|
|
|
27
27
|
const stageAllowed = triggerEventAllowed(project, normalized.triggerEvent);
|
|
28
28
|
const desiredStage = this.resolveDesiredStage(project, normalized, issue, activeStageRun, delegatedToPatchRelay);
|
|
29
29
|
const launchInput = this.resolveLaunchInput(normalized.agentSession);
|
|
30
|
+
const activeAgentSessionId = normalized.agentSession?.id ??
|
|
31
|
+
(!activeStageRun && (desiredStage || (normalized.triggerEvent === "delegateChanged" && !delegatedToPatchRelay)) ? null : undefined);
|
|
30
32
|
const refreshedIssue = this.stores.workflowCoordinator.recordDesiredStage({
|
|
31
33
|
projectId: project.id,
|
|
32
34
|
linearIssueId: normalizedIssue.id,
|
|
@@ -36,7 +38,7 @@ export class WebhookDesiredStageRecorder {
|
|
|
36
38
|
...(normalizedIssue.stateName ? { currentLinearState: normalizedIssue.stateName } : {}),
|
|
37
39
|
...(desiredStage ? { desiredStage } : {}),
|
|
38
40
|
...(options?.eventReceiptId !== undefined ? { desiredReceiptId: options.eventReceiptId } : {}),
|
|
39
|
-
...(
|
|
41
|
+
...(activeAgentSessionId !== undefined ? { activeAgentSessionId } : {}),
|
|
40
42
|
lastWebhookAt: new Date().toISOString(),
|
|
41
43
|
});
|
|
42
44
|
if (launchInput && !activeStageRun && delegatedToPatchRelay && stageAllowed) {
|