patchrelay 0.20.8 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli/watch/App.js +25 -0
- package/dist/cli/watch/HelpBar.js +1 -1
- package/dist/http.js +11 -0
- package/dist/run-orchestrator.js +13 -1
- package/dist/service.js +43 -1
- package/dist/webhook-handler.js +25 -1
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli/watch/App.js
CHANGED
|
@@ -25,6 +25,22 @@ async function postPrompt(baseUrl, issueKey, text, bearerToken) {
|
|
|
25
25
|
return { reason: "Request failed" };
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
async function postStop(baseUrl, issueKey, bearerToken) {
|
|
29
|
+
const headers = { "content-type": "application/json" };
|
|
30
|
+
if (bearerToken)
|
|
31
|
+
headers.authorization = `Bearer ${bearerToken}`;
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(new URL(`/api/issues/${encodeURIComponent(issueKey)}/stop`, baseUrl), {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers,
|
|
36
|
+
signal: AbortSignal.timeout(5000),
|
|
37
|
+
});
|
|
38
|
+
return await response.json();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { reason: "request failed" };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
28
44
|
async function postRetry(baseUrl, issueKey, bearerToken) {
|
|
29
45
|
const headers = { "content-type": "application/json" };
|
|
30
46
|
if (bearerToken)
|
|
@@ -146,6 +162,15 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
146
162
|
else if (input === "p") {
|
|
147
163
|
setPromptMode(true);
|
|
148
164
|
}
|
|
165
|
+
else if (input === "s") {
|
|
166
|
+
if (state.activeDetailKey) {
|
|
167
|
+
setPromptStatus("stopping...");
|
|
168
|
+
void postStop(baseUrl, state.activeDetailKey, bearerToken).then((result) => {
|
|
169
|
+
setPromptStatus(result.ok ? "stop sent" : `stop failed: ${result.reason ?? "unknown"}`);
|
|
170
|
+
setTimeout(() => setPromptStatus(null), 3000);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
149
174
|
else if (input === "j" || key.downArrow) {
|
|
150
175
|
dispatch({ type: "detail-navigate", direction: "next", filtered });
|
|
151
176
|
}
|
|
@@ -7,7 +7,7 @@ const HELP_TEXT = {
|
|
|
7
7
|
};
|
|
8
8
|
export function HelpBar({ view, follow }) {
|
|
9
9
|
const text = view === "detail"
|
|
10
|
-
? `j/k: prev/next Esc: list f: follow ${follow ? "on" : "off"} p: prompt r: retry q: quit`
|
|
10
|
+
? `j/k: prev/next Esc: list f: follow ${follow ? "on" : "off"} p: prompt s: stop r: retry q: quit`
|
|
11
11
|
: HELP_TEXT[view];
|
|
12
12
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: text }) }));
|
|
13
13
|
}
|
package/dist/http.js
CHANGED
|
@@ -325,6 +325,17 @@ export async function buildHttpServer(config, service, logger) {
|
|
|
325
325
|
}
|
|
326
326
|
return reply.send({ ok: true, ...result });
|
|
327
327
|
});
|
|
328
|
+
app.post("/api/issues/:issueKey/stop", async (request, reply) => {
|
|
329
|
+
const issueKey = request.params.issueKey;
|
|
330
|
+
const result = await service.stopIssue(issueKey);
|
|
331
|
+
if (!result) {
|
|
332
|
+
return reply.code(404).send({ ok: false, reason: "issue_not_found" });
|
|
333
|
+
}
|
|
334
|
+
if ("error" in result) {
|
|
335
|
+
return reply.code(409).send({ ok: false, reason: result.error });
|
|
336
|
+
}
|
|
337
|
+
return reply.send({ ok: true, ...result });
|
|
338
|
+
});
|
|
328
339
|
app.get("/api/feed", async (request, reply) => {
|
|
329
340
|
const feedQuery = {
|
|
330
341
|
limit: getPositiveIntegerQueryParam(request, "limit") ?? 50,
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -84,6 +84,7 @@ export class RunOrchestrator {
|
|
|
84
84
|
feed;
|
|
85
85
|
worktreeManager;
|
|
86
86
|
progressThrottle = new Map();
|
|
87
|
+
activeThreadId;
|
|
87
88
|
botIdentity;
|
|
88
89
|
constructor(config, db, codex, linearProvider, enqueueIssue, logger, feed) {
|
|
89
90
|
this.config = config;
|
|
@@ -247,9 +248,18 @@ export class RunOrchestrator {
|
|
|
247
248
|
}
|
|
248
249
|
// ─── Notification handler ─────────────────────────────────────────
|
|
249
250
|
async handleCodexNotification(notification) {
|
|
250
|
-
|
|
251
|
+
// threadId is present on turn-level notifications but NOT on item-level ones.
|
|
252
|
+
// Fall back to the tracked active thread for item/delta notifications.
|
|
253
|
+
let threadId = typeof notification.params.threadId === "string" ? notification.params.threadId : undefined;
|
|
254
|
+
if (!threadId) {
|
|
255
|
+
threadId = this.activeThreadId;
|
|
256
|
+
}
|
|
251
257
|
if (!threadId)
|
|
252
258
|
return;
|
|
259
|
+
// Track the active thread from turn/started so item notifications can find it
|
|
260
|
+
if (notification.method === "turn/started" && threadId) {
|
|
261
|
+
this.activeThreadId = threadId;
|
|
262
|
+
}
|
|
253
263
|
const run = this.db.getRunByThreadId(threadId);
|
|
254
264
|
if (!run)
|
|
255
265
|
return;
|
|
@@ -299,6 +309,7 @@ export class RunOrchestrator {
|
|
|
299
309
|
void this.emitLinearActivity(failedIssue, buildRunFailureActivity(run.runType));
|
|
300
310
|
void this.syncLinearSession(failedIssue, { activeRunType: run.runType });
|
|
301
311
|
this.progressThrottle.delete(run.id);
|
|
312
|
+
this.activeThreadId = undefined;
|
|
302
313
|
return;
|
|
303
314
|
}
|
|
304
315
|
// Complete the run
|
|
@@ -348,6 +359,7 @@ export class RunOrchestrator {
|
|
|
348
359
|
}));
|
|
349
360
|
void this.syncLinearSession(updatedIssue);
|
|
350
361
|
this.progressThrottle.delete(run.id);
|
|
362
|
+
this.activeThreadId = undefined;
|
|
351
363
|
}
|
|
352
364
|
// ─── In-flight progress ──────────────────────────────────────────
|
|
353
365
|
maybeEmitProgressActivity(notification, run) {
|
package/dist/service.js
CHANGED
|
@@ -192,12 +192,20 @@ export class PatchRelayService {
|
|
|
192
192
|
}));
|
|
193
193
|
}
|
|
194
194
|
subscribeCodexNotifications(listener) {
|
|
195
|
+
let trackedThreadId;
|
|
195
196
|
const handler = (notification) => {
|
|
196
|
-
|
|
197
|
+
let threadId = typeof notification.params.threadId === "string"
|
|
197
198
|
? notification.params.threadId
|
|
198
199
|
: typeof notification.params.thread === "object" && notification.params.thread !== null && "id" in notification.params.thread
|
|
199
200
|
? String(notification.params.thread.id)
|
|
200
201
|
: undefined;
|
|
202
|
+
// Item-level notifications lack threadId — use the tracked one from turn/started
|
|
203
|
+
if (!threadId)
|
|
204
|
+
threadId = trackedThreadId;
|
|
205
|
+
if (notification.method === "turn/started" && threadId)
|
|
206
|
+
trackedThreadId = threadId;
|
|
207
|
+
if (notification.method === "turn/completed")
|
|
208
|
+
trackedThreadId = undefined;
|
|
201
209
|
let issueKey;
|
|
202
210
|
let runId;
|
|
203
211
|
if (threadId) {
|
|
@@ -272,6 +280,40 @@ export class PatchRelayService {
|
|
|
272
280
|
return { delivered: false, queued: true };
|
|
273
281
|
}
|
|
274
282
|
}
|
|
283
|
+
async stopIssue(issueKey) {
|
|
284
|
+
const issue = this.db.getIssueByKey(issueKey);
|
|
285
|
+
if (!issue)
|
|
286
|
+
return undefined;
|
|
287
|
+
if (!issue.activeRunId)
|
|
288
|
+
return { error: "No active run to stop" };
|
|
289
|
+
const run = this.db.getRun(issue.activeRunId);
|
|
290
|
+
if (run?.threadId && run.turnId) {
|
|
291
|
+
try {
|
|
292
|
+
await this.codex.steerTurn({
|
|
293
|
+
threadId: run.threadId,
|
|
294
|
+
turnId: run.turnId,
|
|
295
|
+
input: "STOP: The operator has requested this run to halt immediately. Finish your current action, commit any partial progress, and stop.",
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// Turn may already be done
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
this.db.upsertIssue({
|
|
303
|
+
projectId: issue.projectId,
|
|
304
|
+
linearIssueId: issue.linearIssueId,
|
|
305
|
+
factoryState: "awaiting_input",
|
|
306
|
+
});
|
|
307
|
+
this.feed.publish({
|
|
308
|
+
level: "warn",
|
|
309
|
+
kind: "workflow",
|
|
310
|
+
issueKey: issue.issueKey,
|
|
311
|
+
projectId: issue.projectId,
|
|
312
|
+
status: "stopped",
|
|
313
|
+
summary: "Operator stopped the run",
|
|
314
|
+
});
|
|
315
|
+
return { stopped: true };
|
|
316
|
+
}
|
|
275
317
|
retryIssue(issueKey) {
|
|
276
318
|
const issue = this.db.getIssueByKey(issueKey);
|
|
277
319
|
if (!issue)
|
package/dist/webhook-handler.js
CHANGED
|
@@ -306,8 +306,32 @@ export class WebhookHandler {
|
|
|
306
306
|
if (!triggerEventAllowed(project, normalized.triggerEvent))
|
|
307
307
|
return;
|
|
308
308
|
const issue = this.db.getIssue(project.id, normalized.issue.id);
|
|
309
|
-
if (!issue
|
|
309
|
+
if (!issue)
|
|
310
310
|
return;
|
|
311
|
+
// No active run — enqueue a run with the comment as context if appropriate
|
|
312
|
+
if (!issue.activeRunId) {
|
|
313
|
+
const ENQUEUEABLE_STATES = new Set(["pr_open", "changes_requested", "implementing", "delegated"]);
|
|
314
|
+
if (ENQUEUEABLE_STATES.has(issue.factoryState)) {
|
|
315
|
+
const runType = issue.prReviewState === "changes_requested" ? "review_fix" : "implementation";
|
|
316
|
+
this.db.upsertIssue({
|
|
317
|
+
projectId: project.id,
|
|
318
|
+
linearIssueId: normalized.issue.id,
|
|
319
|
+
pendingRunType: runType,
|
|
320
|
+
pendingRunContextJson: JSON.stringify({ userComment: normalized.comment.body.trim() }),
|
|
321
|
+
});
|
|
322
|
+
this.enqueueIssue(project.id, normalized.issue.id);
|
|
323
|
+
this.feed?.publish({
|
|
324
|
+
level: "info",
|
|
325
|
+
kind: "comment",
|
|
326
|
+
projectId: project.id,
|
|
327
|
+
issueKey: trackedIssue?.issueKey,
|
|
328
|
+
status: "enqueued",
|
|
329
|
+
summary: `Comment enqueued ${runType} run`,
|
|
330
|
+
detail: normalized.comment.body.slice(0, 200),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
311
335
|
const run = this.db.getRun(issue.activeRunId);
|
|
312
336
|
if (!run?.threadId || !run.turnId)
|
|
313
337
|
return;
|