patchrelay 0.37.0 → 0.38.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/README.md +47 -9
- package/dist/awaiting-input-reason.js +9 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/cluster-health.js +64 -5
- package/dist/cli/data.js +1 -0
- package/dist/cli/formatters/text.js +5 -1
- package/dist/cli/help.js +1 -1
- package/dist/cli/output.js +2 -0
- package/dist/cli/watch/IssueRow.js +4 -3
- package/dist/cli/watch/StatusBar.js +2 -1
- package/dist/cli/watch/detail-rows.js +4 -3
- package/dist/cli/watch/pr-status.js +2 -1
- package/dist/cli/watch/state-visualization.js +5 -1
- package/dist/db/issue-session-store.js +0 -14
- package/dist/db/issue-store.js +8 -16
- package/dist/db/migrations.js +6 -13
- package/dist/db.js +1 -3
- package/dist/factory-state.js +1 -1
- package/dist/github-webhook-handler.js +95 -54
- package/dist/github-webhooks.js +4 -0
- package/dist/idle-reconciliation.js +38 -22
- package/dist/implementation-outcome-policy.js +3 -1
- package/dist/issue-overview-query.js +8 -0
- package/dist/issue-session-projector.js +1 -0
- package/dist/issue-session.js +8 -0
- package/dist/linear-session-reporting.js +43 -5
- package/dist/linear-session-sync.js +9 -1
- package/dist/linear-status-comment-sync.js +47 -2
- package/dist/linear-workflow-state-sync.js +2 -2
- package/dist/operator-retry-event.js +15 -12
- package/dist/paused-issue-state.js +24 -0
- package/dist/pr-state.js +49 -0
- package/dist/run-launcher.js +0 -1
- package/dist/run-orchestrator.js +2 -5
- package/dist/run-reconciler.js +10 -0
- package/dist/run-recovery-service.js +1 -10
- package/dist/service-issue-actions.js +10 -4
- package/dist/service-startup-recovery.js +9 -6
- package/dist/service.js +0 -1
- package/dist/tracked-issue-list-query.js +6 -2
- package/dist/tracked-issue-projector.js +8 -0
- package/dist/waiting-reason.js +13 -2
- package/dist/webhooks/agent-session-handler.js +9 -1
- package/dist/webhooks/comment-wake-handler.js +12 -0
- package/dist/webhooks/decision-helpers.js +44 -3
- package/dist/webhooks/dependency-readiness-handler.js +1 -0
- package/dist/webhooks/desired-stage-recorder.js +40 -10
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { extractCompletionCheck } from "./completion-check.js";
|
|
2
2
|
import { deriveIssueStatusNote } from "./status-note.js";
|
|
3
3
|
import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
|
|
4
|
+
import { isClosedPrState } from "./pr-state.js";
|
|
5
|
+
import { isUndelegatedPausedIssue } from "./paused-issue-state.js";
|
|
4
6
|
export async function syncVisibleStatusComment(params) {
|
|
5
7
|
const { db, issue, linear, logger, trackedIssue, options } = params;
|
|
6
8
|
try {
|
|
@@ -31,7 +33,12 @@ export function shouldSyncVisibleIssueComment(issue, hasAgentSession) {
|
|
|
31
33
|
|| issue.factoryState === "awaiting_input" || issue.factoryState === "failed" || issue.factoryState === "escalated") {
|
|
32
34
|
return true;
|
|
33
35
|
}
|
|
34
|
-
if ((issue
|
|
36
|
+
if (isUndelegatedPausedIssue(issue)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if ((issue.sessionState === "done" || issue.factoryState === "done")
|
|
40
|
+
&& ((issue.prNumber === undefined && !issue.prUrl)
|
|
41
|
+
|| isClosedPrState(issue.prState))) {
|
|
35
42
|
return true;
|
|
36
43
|
}
|
|
37
44
|
return false;
|
|
@@ -44,11 +51,13 @@ function renderStatusComment(db, issue, trackedIssue, options) {
|
|
|
44
51
|
? (options?.activeRunType ?? activeRun?.runType)
|
|
45
52
|
: undefined;
|
|
46
53
|
const waitingReason = trackedIssue?.waitingReason ?? derivePatchRelayWaitingReason({
|
|
54
|
+
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
47
55
|
...(activeRunType ? { activeRunType } : {}),
|
|
48
56
|
...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
|
|
49
57
|
factoryState: issue.factoryState,
|
|
50
58
|
pendingRunType: issue.pendingRunType,
|
|
51
59
|
...(issue.prNumber !== undefined ? { prNumber: issue.prNumber } : {}),
|
|
60
|
+
...(issue.prState ? { prState: issue.prState } : {}),
|
|
52
61
|
prHeadSha: issue.prHeadSha,
|
|
53
62
|
prReviewState: issue.prReviewState,
|
|
54
63
|
prCheckStatus: issue.prCheckStatus,
|
|
@@ -58,7 +67,15 @@ function renderStatusComment(db, issue, trackedIssue, options) {
|
|
|
58
67
|
const lines = [
|
|
59
68
|
"## PatchRelay status",
|
|
60
69
|
"",
|
|
61
|
-
statusHeadline(trackedIssue
|
|
70
|
+
statusHeadline(trackedIssue
|
|
71
|
+
? {
|
|
72
|
+
...trackedIssue,
|
|
73
|
+
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
74
|
+
prNumber: issue.prNumber,
|
|
75
|
+
prReviewState: issue.prReviewState,
|
|
76
|
+
prCheckStatus: issue.prCheckStatus,
|
|
77
|
+
}
|
|
78
|
+
: issue, activeRunType),
|
|
62
79
|
];
|
|
63
80
|
const statusNote = trackedIssue?.statusNote ?? deriveIssueStatusNote({ issue, latestRun, latestEvent, waitingReason });
|
|
64
81
|
if (waitingReason) {
|
|
@@ -110,12 +127,36 @@ function statusHeadline(issue, activeRunType) {
|
|
|
110
127
|
case "running":
|
|
111
128
|
return issue.prNumber !== undefined ? `PR #${issue.prNumber} is actively running` : "Actively running";
|
|
112
129
|
case "done":
|
|
130
|
+
if (issue.prNumber !== undefined && issue.prState === "merged")
|
|
131
|
+
return `Completed with merged PR #${issue.prNumber}`;
|
|
132
|
+
if (issue.prNumber !== undefined && isClosedPrState(issue.prState))
|
|
133
|
+
return `Completed without merging PR #${issue.prNumber}`;
|
|
113
134
|
return issue.prNumber !== undefined ? `Completed with PR #${issue.prNumber}` : "Completed";
|
|
114
135
|
case "failed":
|
|
115
136
|
return "Needs operator intervention";
|
|
116
137
|
default:
|
|
117
138
|
break;
|
|
118
139
|
}
|
|
140
|
+
if (!issue.delegatedToPatchRelay && issue.prNumber !== undefined) {
|
|
141
|
+
if (issue.factoryState === "awaiting_queue" || issue.prReviewState === "approved") {
|
|
142
|
+
return `PR #${issue.prNumber} is awaiting downstream merge while PatchRelay is paused`;
|
|
143
|
+
}
|
|
144
|
+
if (issue.factoryState === "changes_requested" || issue.prReviewState === "changes_requested") {
|
|
145
|
+
return `PR #${issue.prNumber} has requested changes while PatchRelay is paused`;
|
|
146
|
+
}
|
|
147
|
+
if (issue.factoryState === "repairing_ci" || issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure") {
|
|
148
|
+
return `PR #${issue.prNumber} has failing CI while PatchRelay is paused`;
|
|
149
|
+
}
|
|
150
|
+
return `PR #${issue.prNumber} is awaiting review while PatchRelay is paused`;
|
|
151
|
+
}
|
|
152
|
+
if (!issue.delegatedToPatchRelay) {
|
|
153
|
+
if (issue.factoryState === "implementing") {
|
|
154
|
+
return "Implementation is paused because the issue is undelegated";
|
|
155
|
+
}
|
|
156
|
+
if (issue.factoryState === "delegated") {
|
|
157
|
+
return "Queued to start work while PatchRelay is paused";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
119
160
|
switch (issue.factoryState) {
|
|
120
161
|
case "delegated":
|
|
121
162
|
return "Queued to start work";
|
|
@@ -138,6 +179,10 @@ function statusHeadline(issue, activeRunType) {
|
|
|
138
179
|
case "escalated":
|
|
139
180
|
return "Needs operator intervention";
|
|
140
181
|
case "done":
|
|
182
|
+
if (issue.prNumber !== undefined && issue.prState === "merged")
|
|
183
|
+
return `Completed with merged PR #${issue.prNumber}`;
|
|
184
|
+
if (issue.prNumber !== undefined && isClosedPrState(issue.prState))
|
|
185
|
+
return `Completed without merging PR #${issue.prNumber}`;
|
|
141
186
|
return issue.prNumber !== undefined ? `Completed with PR #${issue.prNumber}` : "Completed";
|
|
142
187
|
default:
|
|
143
188
|
return humanize(issue.factoryState);
|
|
@@ -53,14 +53,14 @@ function resolveDesiredActiveWorkflowState(issue, trackedIssue, options, liveIss
|
|
|
53
53
|
|| trackedIssue?.sessionState === "waiting_input" || trackedIssue?.sessionState === "failed") {
|
|
54
54
|
return resolvePreferredHumanNeededLinearState(liveIssue);
|
|
55
55
|
}
|
|
56
|
-
const activelyWorking = issue.activeRunId !== undefined
|
|
56
|
+
const activelyWorking = issue.delegatedToPatchRelay !== false && (issue.activeRunId !== undefined
|
|
57
57
|
|| options?.activeRunType !== undefined
|
|
58
58
|
|| trackedIssue?.sessionState === "running"
|
|
59
59
|
|| issue.factoryState === "delegated"
|
|
60
60
|
|| issue.factoryState === "implementing"
|
|
61
61
|
|| issue.factoryState === "changes_requested"
|
|
62
62
|
|| issue.factoryState === "repairing_ci"
|
|
63
|
-
|| issue.factoryState === "repairing_queue";
|
|
63
|
+
|| issue.factoryState === "repairing_queue");
|
|
64
64
|
if (activelyWorking) {
|
|
65
65
|
return resolvePreferredImplementingLinearState(liveIssue);
|
|
66
66
|
}
|
|
@@ -9,7 +9,7 @@ function parseObjectJson(value) {
|
|
|
9
9
|
return undefined;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
export function buildOperatorRetryEvent(issue, runType) {
|
|
12
|
+
export function buildOperatorRetryEvent(issue, runType, source = "operator_retry") {
|
|
13
13
|
if (runType === "queue_repair") {
|
|
14
14
|
const queueIncident = parseObjectJson(issue.lastQueueIncidentJson);
|
|
15
15
|
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson);
|
|
@@ -18,9 +18,9 @@ export function buildOperatorRetryEvent(issue, runType) {
|
|
|
18
18
|
eventJson: JSON.stringify({
|
|
19
19
|
...(queueIncident ?? {}),
|
|
20
20
|
...(failureContext ?? {}),
|
|
21
|
-
source
|
|
21
|
+
source,
|
|
22
22
|
}),
|
|
23
|
-
dedupeKey:
|
|
23
|
+
dedupeKey: `${source}:queue_repair:${issue.linearIssueId}:${issue.prHeadSha ?? issue.lastGitHubFailureHeadSha ?? "unknown-sha"}`,
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
if (runType === "ci_repair") {
|
|
@@ -29,9 +29,9 @@ export function buildOperatorRetryEvent(issue, runType) {
|
|
|
29
29
|
eventType: "settled_red_ci",
|
|
30
30
|
eventJson: JSON.stringify({
|
|
31
31
|
...(failureContext ?? {}),
|
|
32
|
-
source
|
|
32
|
+
source,
|
|
33
33
|
}),
|
|
34
|
-
dedupeKey:
|
|
34
|
+
dedupeKey: `${source}:ci_repair:${issue.linearIssueId}:${issue.lastGitHubFailureSignature ?? issue.prHeadSha ?? "unknown-sha"}`,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
if (runType === "review_fix" || runType === "branch_upkeep") {
|
|
@@ -39,20 +39,23 @@ export function buildOperatorRetryEvent(issue, runType) {
|
|
|
39
39
|
eventType: "review_changes_requested",
|
|
40
40
|
eventJson: JSON.stringify({
|
|
41
41
|
reviewBody: runType === "branch_upkeep"
|
|
42
|
-
?
|
|
43
|
-
:
|
|
42
|
+
? `${humanizeSource(source)} requested retry of branch upkeep after requested changes.`
|
|
43
|
+
: `${humanizeSource(source)} requested retry of review-fix work.`,
|
|
44
44
|
...(runType === "branch_upkeep" ? { branchUpkeepRequired: true, wakeReason: "branch_upkeep" } : {}),
|
|
45
|
-
source
|
|
45
|
+
source,
|
|
46
46
|
}),
|
|
47
|
-
dedupeKey:
|
|
47
|
+
dedupeKey: `${source}:${runType}:${issue.linearIssueId}:${issue.prHeadSha ?? "unknown-sha"}`,
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
return {
|
|
51
51
|
eventType: "delegated",
|
|
52
52
|
eventJson: JSON.stringify({
|
|
53
|
-
promptContext:
|
|
54
|
-
source
|
|
53
|
+
promptContext: `${humanizeSource(source)} requested PatchRelay work resume.`,
|
|
54
|
+
source,
|
|
55
55
|
}),
|
|
56
|
-
dedupeKey:
|
|
56
|
+
dedupeKey: `${source}:implementation:${issue.linearIssueId}`,
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
|
+
function humanizeSource(source) {
|
|
60
|
+
return source.replaceAll("_", " ");
|
|
61
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { resolveAwaitingInputReason } from "./awaiting-input-reason.js";
|
|
2
|
+
export function isUndelegatedPausedIssue(issue) {
|
|
3
|
+
return issue.delegatedToPatchRelay === false
|
|
4
|
+
&& issue.factoryState !== "done"
|
|
5
|
+
&& issue.factoryState !== "failed"
|
|
6
|
+
&& issue.factoryState !== "escalated";
|
|
7
|
+
}
|
|
8
|
+
export function isUndelegatedPausedNoPrWork(issue) {
|
|
9
|
+
return isUndelegatedPausedIssue(issue)
|
|
10
|
+
&& issue.prNumber === undefined
|
|
11
|
+
&& (issue.factoryState === "delegated" || issue.factoryState === "implementing");
|
|
12
|
+
}
|
|
13
|
+
export function isResumablePausedLocalWork(params) {
|
|
14
|
+
if (params.issue.delegatedToPatchRelay === false) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (params.issue.prNumber !== undefined) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (params.issue.factoryState === "delegated" || params.issue.factoryState === "implementing") {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return resolveAwaitingInputReason(params) === "paused_local_work";
|
|
24
|
+
}
|
package/dist/pr-state.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { TERMINAL_STATES } from "./factory-state.js";
|
|
2
|
+
export function isOpenPrState(prState) {
|
|
3
|
+
return prState === undefined || prState === "open";
|
|
4
|
+
}
|
|
5
|
+
export function hasOpenPr(prNumber, prState) {
|
|
6
|
+
// Transitional compatibility: older rows may still have a tracked PR number
|
|
7
|
+
// before webhook/reconciliation has populated pr_state.
|
|
8
|
+
return prNumber !== undefined && isOpenPrState(prState);
|
|
9
|
+
}
|
|
10
|
+
export function isClosedPrState(prState) {
|
|
11
|
+
return prState === "closed";
|
|
12
|
+
}
|
|
13
|
+
export function isCompletedLinearState(currentLinearStateType, currentLinearState) {
|
|
14
|
+
return currentLinearStateType === "completed"
|
|
15
|
+
|| currentLinearState?.trim().toLowerCase() === "done";
|
|
16
|
+
}
|
|
17
|
+
export function isIssueCompleted(issue) {
|
|
18
|
+
return issue.factoryState === "done" || isCompletedLinearState(issue.currentLinearStateType, issue.currentLinearState);
|
|
19
|
+
}
|
|
20
|
+
export function isIssueTerminal(issue) {
|
|
21
|
+
return issue.factoryState !== undefined && TERMINAL_STATES.has(issue.factoryState);
|
|
22
|
+
}
|
|
23
|
+
export function resolveClosedPrDisposition(issue) {
|
|
24
|
+
if (isIssueCompleted(issue))
|
|
25
|
+
return "done";
|
|
26
|
+
if (isIssueTerminal(issue))
|
|
27
|
+
return "terminal";
|
|
28
|
+
return "redelegate";
|
|
29
|
+
}
|
|
30
|
+
export function resolveClosedPrFactoryState(issue) {
|
|
31
|
+
const disposition = resolveClosedPrDisposition(issue);
|
|
32
|
+
if (disposition === "done")
|
|
33
|
+
return "done";
|
|
34
|
+
if (disposition === "terminal")
|
|
35
|
+
return issue.factoryState;
|
|
36
|
+
return "delegated";
|
|
37
|
+
}
|
|
38
|
+
export function buildClosedPrCleanupFields() {
|
|
39
|
+
return {
|
|
40
|
+
prReviewState: null,
|
|
41
|
+
prCheckStatus: null,
|
|
42
|
+
lastBlockingReviewHeadSha: null,
|
|
43
|
+
lastGitHubCiSnapshotHeadSha: null,
|
|
44
|
+
lastGitHubCiSnapshotGateCheckName: null,
|
|
45
|
+
lastGitHubCiSnapshotGateCheckStatus: null,
|
|
46
|
+
lastGitHubCiSnapshotJson: null,
|
|
47
|
+
lastGitHubCiSnapshotSettledAt: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/run-launcher.js
CHANGED
|
@@ -106,7 +106,6 @@ export class RunLauncher {
|
|
|
106
106
|
});
|
|
107
107
|
this.db.issueSessions.consumeIssueSessionEvents(params.item.projectId, params.item.issueId, freshWake.eventIds, created.id);
|
|
108
108
|
this.db.issueSessions.setIssueSessionLastWakeReason(params.item.projectId, params.item.issueId, freshWake.wakeReason ?? null);
|
|
109
|
-
this.db.issueSessions.setBranchOwnerWithLease({ projectId: params.item.projectId, linearIssueId: params.item.issueId, leaseId: params.leaseId }, "patchrelay");
|
|
110
109
|
return created;
|
|
111
110
|
});
|
|
112
111
|
}
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -4,7 +4,7 @@ import { CompletionCheckService } from "./completion-check.js";
|
|
|
4
4
|
import { WorktreeManager } from "./worktree-manager.js";
|
|
5
5
|
import { MergedLinearCompletionReconciler } from "./merged-linear-completion-reconciler.js";
|
|
6
6
|
import { QueueHealthMonitor } from "./queue-health-monitor.js";
|
|
7
|
-
import { IdleIssueReconciler
|
|
7
|
+
import { IdleIssueReconciler } from "./idle-reconciliation.js";
|
|
8
8
|
import { LinearSessionSync } from "./linear-session-sync.js";
|
|
9
9
|
import { IssueSessionLeaseService } from "./issue-session-lease-service.js";
|
|
10
10
|
import { InterruptedRunRecovery } from "./interrupted-run-recovery.js";
|
|
@@ -73,7 +73,7 @@ export class RunOrchestrator {
|
|
|
73
73
|
this.runFinalizer = new RunFinalizer(db, logger, this.linearSync, this.enqueueIssue, (projectId, linearIssueId, fn) => this.withHeldIssueSessionLease(projectId, linearIssueId, fn), (projectId, linearIssueId) => this.releaseIssueSessionLease(projectId, linearIssueId), (lease, issue, runType, context, dedupeScope) => this.appendWakeEventWithLease(lease, issue, runType, context, dedupeScope), (run, message, nextState) => this.failRunAndClear(run, message, nextState), this.runCompletionPolicy, this.completionCheck, feed);
|
|
74
74
|
this.runLauncher = new RunLauncher(config, db, codex, logger, this.worktreeManager);
|
|
75
75
|
this.runNotificationHandler = new RunNotificationHandler(config, db, logger, this.linearSync, this.runFinalizer, (threadId, maxRetries) => this.readThreadWithRetry(threadId, maxRetries), (projectId, linearIssueId, fn) => this.withHeldIssueSessionLease(projectId, linearIssueId, fn), (projectId, linearIssueId) => this.heartbeatIssueSessionLease(projectId, linearIssueId), (projectId, linearIssueId) => this.releaseIssueSessionLease(projectId, linearIssueId), feed);
|
|
76
|
-
this.runRecovery = new RunRecoveryService(db, logger, this.linearSync, (projectId, linearIssueId, fn) => this.withHeldIssueSessionLease(projectId, linearIssueId, fn), (projectId, linearIssueId) => this.getHeldIssueSessionLease(projectId, linearIssueId), (lease, issue, runType, context, dedupeScope) => this.appendWakeEventWithLease(lease, issue, runType, context, dedupeScope), (projectId, linearIssueId) => this.releaseIssueSessionLease(projectId, linearIssueId), (projectId, issueId) => this.enqueueIssue(projectId, issueId),
|
|
76
|
+
this.runRecovery = new RunRecoveryService(db, logger, this.linearSync, (projectId, linearIssueId, fn) => this.withHeldIssueSessionLease(projectId, linearIssueId, fn), (projectId, linearIssueId) => this.getHeldIssueSessionLease(projectId, linearIssueId), (lease, issue, runType, context, dedupeScope) => this.appendWakeEventWithLease(lease, issue, runType, context, dedupeScope), (projectId, linearIssueId) => this.releaseIssueSessionLease(projectId, linearIssueId), (projectId, issueId) => this.enqueueIssue(projectId, issueId), feed);
|
|
77
77
|
this.interruptedRunRecovery = new InterruptedRunRecovery(db, logger, this.linearSync, (projectId, linearIssueId, fn) => this.withHeldIssueSessionLease(projectId, linearIssueId, fn), (projectId, linearIssueId) => this.releaseIssueSessionLease(projectId, linearIssueId), (run, message, nextState) => this.failRunAndClear(run, message, nextState), (issue) => this.restoreIdleWorktree(issue), this.runCompletionPolicy, (projectId, issueId) => this.enqueueIssue(projectId, issueId), feed);
|
|
78
78
|
this.runReconciler = new RunReconciler(db, logger, linearProvider, this.linearSync, this.interruptedRunRecovery, this.runFinalizer, (projectId, linearIssueId, fn) => this.withHeldIssueSessionLease(projectId, linearIssueId, fn), (projectId, linearIssueId) => this.releaseIssueSessionLease(projectId, linearIssueId), (threadId, maxRetries) => this.readThreadWithRetry(threadId, maxRetries), (issue, runType, reason) => this.recoverOrEscalate(issue, runType, reason), feed);
|
|
79
79
|
this.runWakePlanner = new RunWakePlanner(db);
|
|
@@ -302,9 +302,6 @@ export class RunOrchestrator {
|
|
|
302
302
|
nextState,
|
|
303
303
|
});
|
|
304
304
|
}
|
|
305
|
-
resolveBranchOwnerForStateTransition(newState, pendingRunType) {
|
|
306
|
-
return resolveBranchOwnerForStateTransition(newState, pendingRunType);
|
|
307
|
-
}
|
|
308
305
|
async resolveRequestedChangesWakeContext(issue, runType, context) {
|
|
309
306
|
return await this.runCompletionPolicy.resolveRequestedChangesWakeContext(issue, runType, context);
|
|
310
307
|
}
|
package/dist/run-reconciler.js
CHANGED
|
@@ -31,6 +31,16 @@ export class RunReconciler {
|
|
|
31
31
|
async reconcile(params) {
|
|
32
32
|
const { run, issue, recoveryLease } = params;
|
|
33
33
|
const acquiredRecoveryLease = recoveryLease === true;
|
|
34
|
+
if (!issue.delegatedToPatchRelay) {
|
|
35
|
+
this.withHeldLease(run.projectId, run.linearIssueId, () => {
|
|
36
|
+
this.db.runs.finishRun(run.id, { status: "released", failureReason: "Issue was un-delegated during active run" });
|
|
37
|
+
this.db.issues.upsertIssue({ projectId: run.projectId, linearIssueId: run.linearIssueId, activeRunId: null, factoryState: issue.factoryState });
|
|
38
|
+
});
|
|
39
|
+
const pausedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
40
|
+
void this.linearSync.syncSession(pausedIssue, { activeRunType: run.runType });
|
|
41
|
+
this.releaseLease(run.projectId, run.linearIssueId);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
34
44
|
if (TERMINAL_STATES.has(issue.factoryState)) {
|
|
35
45
|
this.withHeldLease(run.projectId, run.linearIssueId, () => {
|
|
36
46
|
this.db.runs.finishRun(run.id, { status: "released", failureReason: "Issue reached terminal state during active run" });
|
|
@@ -10,9 +10,8 @@ export class RunRecoveryService {
|
|
|
10
10
|
appendWakeEventWithLease;
|
|
11
11
|
releaseLease;
|
|
12
12
|
enqueueIssue;
|
|
13
|
-
resolveBranchOwnerForStateTransition;
|
|
14
13
|
feed;
|
|
15
|
-
constructor(db, logger, linearSync, withHeldLease, getHeldLease, appendWakeEventWithLease, releaseLease, enqueueIssue,
|
|
14
|
+
constructor(db, logger, linearSync, withHeldLease, getHeldLease, appendWakeEventWithLease, releaseLease, enqueueIssue, feed) {
|
|
16
15
|
this.db = db;
|
|
17
16
|
this.logger = logger;
|
|
18
17
|
this.linearSync = linearSync;
|
|
@@ -21,7 +20,6 @@ export class RunRecoveryService {
|
|
|
21
20
|
this.appendWakeEventWithLease = appendWakeEventWithLease;
|
|
22
21
|
this.releaseLease = releaseLease;
|
|
23
22
|
this.enqueueIssue = enqueueIssue;
|
|
24
|
-
this.resolveBranchOwnerForStateTransition = resolveBranchOwnerForStateTransition;
|
|
25
23
|
this.feed = feed;
|
|
26
24
|
}
|
|
27
25
|
recoverOrEscalate(params) {
|
|
@@ -189,13 +187,6 @@ export class RunRecoveryService {
|
|
|
189
187
|
activeRunId: null,
|
|
190
188
|
factoryState: nextState,
|
|
191
189
|
});
|
|
192
|
-
const branchOwner = this.resolveBranchOwnerForStateTransition(nextState);
|
|
193
|
-
if (branchOwner) {
|
|
194
|
-
const heldLease = this.getHeldLease(run.projectId, run.linearIssueId);
|
|
195
|
-
if (heldLease) {
|
|
196
|
-
this.db.issueSessions.setBranchOwnerWithLease(heldLease, branchOwner);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
190
|
return true;
|
|
200
191
|
});
|
|
201
192
|
if (!updated) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildOperatorRetryEvent } from "./operator-retry-event.js";
|
|
2
|
+
import { hasOpenPr } from "./pr-state.js";
|
|
2
3
|
export class ServiceIssueActions {
|
|
3
4
|
db;
|
|
4
5
|
codex;
|
|
@@ -16,6 +17,9 @@ export class ServiceIssueActions {
|
|
|
16
17
|
const issue = this.db.issues.getIssueByKey(issueKey);
|
|
17
18
|
if (!issue)
|
|
18
19
|
return undefined;
|
|
20
|
+
if (!issue.delegatedToPatchRelay && !issue.activeRunId) {
|
|
21
|
+
return { error: "Issue is undelegated from PatchRelay; delegate it again before prompting work" };
|
|
22
|
+
}
|
|
19
23
|
this.feed.publish({
|
|
20
24
|
level: "info",
|
|
21
25
|
kind: "comment",
|
|
@@ -94,6 +98,8 @@ export class ServiceIssueActions {
|
|
|
94
98
|
const issue = this.db.issues.getIssueByKey(issueKey);
|
|
95
99
|
if (!issue)
|
|
96
100
|
return undefined;
|
|
101
|
+
if (!issue.delegatedToPatchRelay)
|
|
102
|
+
return { error: "Issue is undelegated from PatchRelay; delegate it again before retrying" };
|
|
97
103
|
if (issue.activeRunId)
|
|
98
104
|
return { error: "Issue already has an active run" };
|
|
99
105
|
const issueSession = this.db.issueSessions.getIssueSession(issue.projectId, issue.linearIssueId);
|
|
@@ -107,21 +113,21 @@ export class ServiceIssueActions {
|
|
|
107
113
|
}
|
|
108
114
|
let runType = "implementation";
|
|
109
115
|
let factoryState = "delegated";
|
|
110
|
-
if (issue.prNumber && issue.lastGitHubFailureSource === "queue_eviction") {
|
|
116
|
+
if (hasOpenPr(issue.prNumber, issue.prState) && issue.lastGitHubFailureSource === "queue_eviction") {
|
|
111
117
|
runType = "queue_repair";
|
|
112
118
|
factoryState = "repairing_queue";
|
|
113
119
|
}
|
|
114
|
-
else if (issue.prNumber && (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure" || issue.lastGitHubFailureSource === "branch_ci")) {
|
|
120
|
+
else if (hasOpenPr(issue.prNumber, issue.prState) && (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure" || issue.lastGitHubFailureSource === "branch_ci")) {
|
|
115
121
|
runType = "ci_repair";
|
|
116
122
|
factoryState = "repairing_ci";
|
|
117
123
|
}
|
|
118
|
-
else if (issue.prNumber && issue.prReviewState === "changes_requested") {
|
|
124
|
+
else if (hasOpenPr(issue.prNumber, issue.prState) && issue.prReviewState === "changes_requested") {
|
|
119
125
|
runType = issue.pendingRunType === "branch_upkeep" || issueSession?.lastRunType === "branch_upkeep"
|
|
120
126
|
? "branch_upkeep"
|
|
121
127
|
: "review_fix";
|
|
122
128
|
factoryState = "changes_requested";
|
|
123
129
|
}
|
|
124
|
-
else if (issue.prNumber) {
|
|
130
|
+
else if (hasOpenPr(issue.prNumber, issue.prState)) {
|
|
125
131
|
runType = "implementation";
|
|
126
132
|
factoryState = "implementing";
|
|
127
133
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isResumablePausedLocalWork } from "./paused-issue-state.js";
|
|
1
2
|
export class ServiceStartupRecovery {
|
|
2
3
|
db;
|
|
3
4
|
linearProvider;
|
|
@@ -65,12 +66,14 @@ export class ServiceStartupRecovery {
|
|
|
65
66
|
});
|
|
66
67
|
const delegated = liveIssue.delegateId === installation.actorId;
|
|
67
68
|
const unresolvedBlockers = this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
|
|
68
|
-
const
|
|
69
|
-
|
|
69
|
+
const latestRun = this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
70
|
+
const shouldRecoverPausedLocalWork = delegated
|
|
71
|
+
&& isResumablePausedLocalWork({ issue, latestRun })
|
|
70
72
|
&& this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) === undefined;
|
|
71
73
|
const updated = this.db.issues.upsertIssue({
|
|
72
74
|
projectId: issue.projectId,
|
|
73
75
|
linearIssueId: issue.linearIssueId,
|
|
76
|
+
delegatedToPatchRelay: delegated,
|
|
74
77
|
...(liveIssue.identifier ? { issueKey: liveIssue.identifier } : {}),
|
|
75
78
|
...(liveIssue.title ? { title: liveIssue.title } : {}),
|
|
76
79
|
...(liveIssue.description ? { description: liveIssue.description } : {}),
|
|
@@ -79,9 +82,9 @@ export class ServiceStartupRecovery {
|
|
|
79
82
|
...(liveIssue.estimate != null ? { estimate: liveIssue.estimate } : {}),
|
|
80
83
|
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
81
84
|
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
82
|
-
...(
|
|
85
|
+
...(shouldRecoverPausedLocalWork ? { factoryState: "delegated" } : {}),
|
|
83
86
|
});
|
|
84
|
-
if (!
|
|
87
|
+
if (!shouldRecoverPausedLocalWork) {
|
|
85
88
|
continue;
|
|
86
89
|
}
|
|
87
90
|
if (unresolvedBlockers === 0) {
|
|
@@ -94,10 +97,10 @@ export class ServiceStartupRecovery {
|
|
|
94
97
|
if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
95
98
|
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
96
99
|
}
|
|
97
|
-
this.logger.info({ issueKey: updated.issueKey }, "Recovered delegated issue from
|
|
100
|
+
this.logger.info({ issueKey: updated.issueKey }, "Recovered delegated issue from paused local-work state and re-queued implementation");
|
|
98
101
|
}
|
|
99
102
|
else {
|
|
100
|
-
this.logger.info({ issueKey: updated.issueKey, unresolvedBlockers }, "Recovered delegated blocked issue from
|
|
103
|
+
this.logger.info({ issueKey: updated.issueKey, unresolvedBlockers }, "Recovered delegated blocked issue from paused local-work state");
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
}
|
package/dist/service.js
CHANGED
|
@@ -103,7 +103,6 @@ export class PatchRelayService {
|
|
|
103
103
|
const identity = this.githubAppTokenManager.botIdentity();
|
|
104
104
|
if (identity) {
|
|
105
105
|
this.orchestrator.botIdentity = identity;
|
|
106
|
-
this.githubWebhookHandler.setPatchRelayAuthorLogins([identity.name]);
|
|
107
106
|
}
|
|
108
107
|
}
|
|
109
108
|
await this.runtime.start();
|
|
@@ -80,9 +80,9 @@ export class TrackedIssueListQuery {
|
|
|
80
80
|
const rows = this.db.connection
|
|
81
81
|
.prepare(`SELECT
|
|
82
82
|
s.project_id, s.linear_issue_id, s.issue_key, i.title,
|
|
83
|
-
i.current_linear_state, i.factory_state, s.session_state, s.waiting_reason, s.summary_text, s.updated_at,
|
|
83
|
+
i.current_linear_state, i.factory_state, i.delegated_to_patchrelay, s.session_state, s.waiting_reason, s.summary_text, s.updated_at,
|
|
84
84
|
i.pending_run_type,
|
|
85
|
-
i.pr_number, i.pr_head_sha, i.pr_review_state, i.pr_check_status, i.last_blocking_review_head_sha,
|
|
85
|
+
i.pr_number, i.pr_state, i.pr_head_sha, i.pr_review_state, i.pr_check_status, i.last_blocking_review_head_sha,
|
|
86
86
|
i.last_github_ci_snapshot_json,
|
|
87
87
|
i.last_github_failure_source,
|
|
88
88
|
i.last_github_failure_head_sha,
|
|
@@ -157,6 +157,7 @@ export class TrackedIssueListQuery {
|
|
|
157
157
|
const readyForExecution = isIssueSessionReadyForExecution({
|
|
158
158
|
...(typeof row.session_state === "string" ? { sessionState: String(row.session_state) } : {}),
|
|
159
159
|
factoryState: String(row.factory_state ?? "delegated"),
|
|
160
|
+
...(row.delegated_to_patchrelay !== null ? { delegatedToPatchRelay: Number(row.delegated_to_patchrelay) !== 0 } : {}),
|
|
160
161
|
...(row.active_run_type !== null ? { activeRunId: 1 } : {}),
|
|
161
162
|
blockedByCount,
|
|
162
163
|
hasPendingWake,
|
|
@@ -175,11 +176,13 @@ export class TrackedIssueListQuery {
|
|
|
175
176
|
? row.summary_text
|
|
176
177
|
: undefined;
|
|
177
178
|
const waitingReason = sessionWaitingReason ?? derivePatchRelayWaitingReason({
|
|
179
|
+
...(row.delegated_to_patchrelay !== null ? { delegatedToPatchRelay: Number(row.delegated_to_patchrelay) !== 0 } : {}),
|
|
178
180
|
...(row.active_run_type !== null ? { activeRunType: String(row.active_run_type) } : {}),
|
|
179
181
|
blockedByKeys,
|
|
180
182
|
factoryState: String(row.factory_state ?? "delegated"),
|
|
181
183
|
...(row.pending_run_type !== null ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
182
184
|
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
185
|
+
...(row.pr_state !== null ? { prState: String(row.pr_state) } : {}),
|
|
183
186
|
...(row.pr_head_sha !== null ? { prHeadSha: String(row.pr_head_sha) } : {}),
|
|
184
187
|
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
185
188
|
...(row.pr_check_status !== null ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
@@ -242,6 +245,7 @@ export class TrackedIssueListQuery {
|
|
|
242
245
|
...(row.latest_run_type !== null ? { latestRunType: String(row.latest_run_type) } : {}),
|
|
243
246
|
...(row.latest_run_status !== null ? { latestRunStatus: String(row.latest_run_status) } : {}),
|
|
244
247
|
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
248
|
+
...(row.pr_state !== null ? { prState: String(row.pr_state) } : {}),
|
|
245
249
|
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
246
250
|
...(row.pr_check_status !== null ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
247
251
|
...(prChecksSummary ? { prChecksSummary } : {}),
|
|
@@ -10,11 +10,13 @@ export function buildTrackedIssueRecord(params) {
|
|
|
10
10
|
const failureContext = parseGitHubFailureContext(params.issue.lastGitHubFailureContextJson);
|
|
11
11
|
const blockedByKeys = unresolvedBlockedBy.map((entry) => entry.blockerIssueKey ?? entry.blockerLinearIssueId);
|
|
12
12
|
const waitingReason = derivePatchRelayWaitingReason({
|
|
13
|
+
delegatedToPatchRelay: params.issue.delegatedToPatchRelay,
|
|
13
14
|
...(params.issue.activeRunId !== undefined ? { activeRunId: params.issue.activeRunId } : {}),
|
|
14
15
|
blockedByKeys,
|
|
15
16
|
factoryState: params.issue.factoryState,
|
|
16
17
|
pendingRunType: params.issue.pendingRunType,
|
|
17
18
|
prNumber: params.issue.prNumber,
|
|
19
|
+
prState: params.issue.prState,
|
|
18
20
|
prHeadSha: params.issue.prHeadSha,
|
|
19
21
|
prReviewState: params.issue.prReviewState,
|
|
20
22
|
prCheckStatus: params.issue.prCheckStatus,
|
|
@@ -39,6 +41,7 @@ export function buildTrackedIssueRecord(params) {
|
|
|
39
41
|
id: params.issue.id,
|
|
40
42
|
projectId: params.issue.projectId,
|
|
41
43
|
linearIssueId: params.issue.linearIssueId,
|
|
44
|
+
delegatedToPatchRelay: params.issue.delegatedToPatchRelay,
|
|
42
45
|
...(params.issue.issueKey ? { issueKey: params.issue.issueKey } : {}),
|
|
43
46
|
...(params.issue.title ? { title: params.issue.title } : {}),
|
|
44
47
|
...(params.issue.url ? { issueUrl: params.issue.url } : {}),
|
|
@@ -46,11 +49,16 @@ export function buildTrackedIssueRecord(params) {
|
|
|
46
49
|
...(params.issue.currentLinearState ? { currentLinearState: params.issue.currentLinearState } : {}),
|
|
47
50
|
...(params.session?.sessionState ? { sessionState: params.session.sessionState } : {}),
|
|
48
51
|
factoryState: params.issue.factoryState,
|
|
52
|
+
...(params.issue.prNumber !== undefined ? { prNumber: params.issue.prNumber } : {}),
|
|
53
|
+
...(params.issue.prState ? { prState: params.issue.prState } : {}),
|
|
54
|
+
...(params.issue.prReviewState ? { prReviewState: params.issue.prReviewState } : {}),
|
|
55
|
+
...(params.issue.prCheckStatus ? { prCheckStatus: params.issue.prCheckStatus } : {}),
|
|
49
56
|
blockedByCount: unresolvedBlockedBy.length,
|
|
50
57
|
blockedByKeys,
|
|
51
58
|
readyForExecution: isIssueSessionReadyForExecution({
|
|
52
59
|
sessionState: params.session?.sessionState,
|
|
53
60
|
factoryState: params.issue.factoryState,
|
|
61
|
+
delegatedToPatchRelay: params.issue.delegatedToPatchRelay,
|
|
54
62
|
activeRunId: params.issue.activeRunId,
|
|
55
63
|
blockedByCount: unresolvedBlockedBy.length,
|
|
56
64
|
hasPendingWake: params.hasPendingWake,
|
package/dist/waiting-reason.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { hasOpenPr } from "./pr-state.js";
|
|
1
2
|
export const PATCHRELAY_WAITING_REASONS = {
|
|
2
3
|
activeWork: "PatchRelay is actively working",
|
|
4
|
+
automationPaused: "PatchRelay automation is paused because the issue is undelegated",
|
|
5
|
+
automationPausedDownstream: "PatchRelay automation is paused; downstream merge may still continue until the PR is closed",
|
|
3
6
|
finalizingPublishedPr: "PatchRelay is finalizing a published PR",
|
|
4
7
|
finalizingMergedChange: "PatchRelay is finalizing a merged change",
|
|
5
8
|
waitingForOperatorInput: "Waiting on operator input",
|
|
@@ -13,8 +16,13 @@ export const PATCHRELAY_WAITING_REASONS = {
|
|
|
13
16
|
waitingForExternalReview: "Waiting on external review",
|
|
14
17
|
};
|
|
15
18
|
export function derivePatchRelayWaitingReason(params) {
|
|
19
|
+
if (params.delegatedToPatchRelay === false && params.factoryState !== "done" && params.factoryState !== "failed" && params.factoryState !== "escalated") {
|
|
20
|
+
return params.factoryState === "awaiting_queue" || (hasLiveOpenPr(params.prNumber, params.prState) && params.prReviewState === "approved")
|
|
21
|
+
? PATCHRELAY_WAITING_REASONS.automationPausedDownstream
|
|
22
|
+
: PATCHRELAY_WAITING_REASONS.automationPaused;
|
|
23
|
+
}
|
|
16
24
|
if (params.activeRunType) {
|
|
17
|
-
if (params.prNumber
|
|
25
|
+
if (hasOpenPr(params.prNumber, params.prState) && (params.factoryState === "pr_open" || params.factoryState === "awaiting_queue")) {
|
|
18
26
|
return PATCHRELAY_WAITING_REASONS.finalizingPublishedPr;
|
|
19
27
|
}
|
|
20
28
|
if (params.factoryState === "done") {
|
|
@@ -66,7 +74,7 @@ export function derivePatchRelayWaitingReason(params) {
|
|
|
66
74
|
if (params.prReviewState === "approved") {
|
|
67
75
|
return PATCHRELAY_WAITING_REASONS.waitingForDownstreamAutomation;
|
|
68
76
|
}
|
|
69
|
-
if (params.prNumber
|
|
77
|
+
if (hasOpenPr(params.prNumber, params.prState)) {
|
|
70
78
|
return PATCHRELAY_WAITING_REASONS.waitingForExternalReview;
|
|
71
79
|
}
|
|
72
80
|
if (params.pendingRunType) {
|
|
@@ -77,3 +85,6 @@ export function derivePatchRelayWaitingReason(params) {
|
|
|
77
85
|
function humanize(value) {
|
|
78
86
|
return value.replaceAll("_", " ");
|
|
79
87
|
}
|
|
88
|
+
function hasLiveOpenPr(prNumber, prState) {
|
|
89
|
+
return prNumber !== undefined && (prState === undefined || prState === "open");
|
|
90
|
+
}
|
|
@@ -26,6 +26,7 @@ export class AgentSessionHandler {
|
|
|
26
26
|
return;
|
|
27
27
|
const existingIssue = this.db.issues.getIssue(project.id, normalized.issue.id);
|
|
28
28
|
const activeRun = existingIssue?.activeRunId ? this.db.runs.getRunById(existingIssue.activeRunId) : undefined;
|
|
29
|
+
const automationEnabled = delegated || existingIssue?.delegatedToPatchRelay === true;
|
|
29
30
|
if (normalized.triggerEvent === "agentSessionCreated") {
|
|
30
31
|
if (!delegated) {
|
|
31
32
|
const latestIssue = this.db.issues.getIssue(project.id, normalized.issue.id);
|
|
@@ -70,6 +71,13 @@ export class AgentSessionHandler {
|
|
|
70
71
|
if (!triggerEventAllowed(project, normalized.triggerEvent))
|
|
71
72
|
return;
|
|
72
73
|
const promptBody = normalized.agentSession.promptBody?.trim();
|
|
74
|
+
if (!automationEnabled && promptBody && existingIssue) {
|
|
75
|
+
await this.publishAgentActivity(linear, normalized.agentSession.id, {
|
|
76
|
+
type: "thought",
|
|
77
|
+
body: "PatchRelay is paused because the issue is undelegated.",
|
|
78
|
+
}, { ephemeral: true });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
73
81
|
if (activeRun && promptBody && activeRun.threadId && activeRun.turnId) {
|
|
74
82
|
const input = `New Linear agent prompt received while you are working.\n\n${promptBody}`;
|
|
75
83
|
try {
|
|
@@ -99,7 +107,7 @@ export class AgentSessionHandler {
|
|
|
99
107
|
await this.publishAgentActivity(linear, normalized.agentSession.id, buildPromptDeliveredThought(activeRun.runType), { ephemeral: true });
|
|
100
108
|
return;
|
|
101
109
|
}
|
|
102
|
-
if (promptBody && existingIssue &&
|
|
110
|
+
if (promptBody && existingIssue && automationEnabled) {
|
|
103
111
|
const hadPendingWake = this.db.issueSessions.peekIssueSessionWake(project.id, normalized.issue.id) !== undefined;
|
|
104
112
|
const directReply = params.isDirectReplyToOutstandingQuestion(existingIssue);
|
|
105
113
|
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(project.id, normalized.issue.id, {
|
|
@@ -40,6 +40,18 @@ export class CommentWakeHandler {
|
|
|
40
40
|
});
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
|
+
if (!issue.delegatedToPatchRelay) {
|
|
44
|
+
this.feed?.publish({
|
|
45
|
+
level: "info",
|
|
46
|
+
kind: "comment",
|
|
47
|
+
projectId: project.id,
|
|
48
|
+
issueKey: trackedIssue?.issueKey,
|
|
49
|
+
status: "ignored_undelegated",
|
|
50
|
+
summary: "Ignored comment because the issue is undelegated",
|
|
51
|
+
detail: trimmedBody.slice(0, 200),
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
43
55
|
if (!issue.activeRunId) {
|
|
44
56
|
if (ENQUEUEABLE_STATES.has(issue.factoryState)) {
|
|
45
57
|
const directReply = params.isDirectReplyToOutstandingQuestion(issue);
|