patchrelay 0.69.3 → 0.69.5
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/db/issue-session-store.js +2 -13
- package/dist/db.js +5 -81
- package/dist/idle-reconciliation.js +1 -1
- package/dist/interrupted-run-recovery.js +1 -1
- package/dist/issue-overview-query.js +1 -1
- package/dist/run-wake-planner.js +1 -1
- package/dist/service-issue-actions.js +1 -1
- package/dist/service-startup-recovery.js +2 -2
- package/dist/tracked-issue-list-query.js +1 -1
- package/dist/tracked-issue-query.js +5 -3
- package/dist/wake-dispatcher.js +2 -2
- package/dist/webhook-handler.js +1 -1
- package/dist/webhooks/desired-stage-recorder.js +30 -116
- package/dist/webhooks/issue-webhook-workflow-planner.js +124 -0
- package/dist/workflow-wake-resolver.js +108 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -6,14 +6,12 @@ export class IssueSessionStore {
|
|
|
6
6
|
mapIssueSessionEventRow;
|
|
7
7
|
issues;
|
|
8
8
|
runs;
|
|
9
|
-
|
|
10
|
-
constructor(connection, mapIssueSessionRow, mapIssueSessionEventRow, issues, runs, deriveImplicitReactiveWake) {
|
|
9
|
+
constructor(connection, mapIssueSessionRow, mapIssueSessionEventRow, issues, runs) {
|
|
11
10
|
this.connection = connection;
|
|
12
11
|
this.mapIssueSessionRow = mapIssueSessionRow;
|
|
13
12
|
this.mapIssueSessionEventRow = mapIssueSessionEventRow;
|
|
14
13
|
this.issues = issues;
|
|
15
14
|
this.runs = runs;
|
|
16
|
-
this.deriveImplicitReactiveWake = deriveImplicitReactiveWake;
|
|
17
15
|
}
|
|
18
16
|
getIssueSession(projectId, linearIssueId) {
|
|
19
17
|
const row = this.connection
|
|
@@ -108,16 +106,7 @@ export class IssueSessionStore {
|
|
|
108
106
|
resumeThread: plan.resumeThread,
|
|
109
107
|
};
|
|
110
108
|
}
|
|
111
|
-
|
|
112
|
-
if (!implicitWake)
|
|
113
|
-
return undefined;
|
|
114
|
-
return {
|
|
115
|
-
eventIds: [],
|
|
116
|
-
runType: implicitWake.runType,
|
|
117
|
-
context: implicitWake.context,
|
|
118
|
-
wakeReason: implicitWake.wakeReason,
|
|
119
|
-
resumeThread: false,
|
|
120
|
-
};
|
|
109
|
+
return undefined;
|
|
121
110
|
}
|
|
122
111
|
acquireIssueSessionLease(params) {
|
|
123
112
|
const now = params.now ?? isoNow();
|
package/dist/db.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { deriveIssueSessionReactiveIntent, } from "./issue-session.js";
|
|
2
1
|
import {} from "./issue-session-events.js";
|
|
3
2
|
import { IssueStore } from "./db/issue-store.js";
|
|
4
3
|
import { IssueSessionStore } from "./db/issue-session-store.js";
|
|
@@ -11,84 +10,7 @@ import { runPatchRelayMigrations } from "./db/migrations.js";
|
|
|
11
10
|
import { SqliteConnection } from "./db/shared.js";
|
|
12
11
|
import { syncIssueSessionFromIssue } from "./issue-session-projector.js";
|
|
13
12
|
import { TrackedIssueQuery } from "./tracked-issue-query.js";
|
|
14
|
-
|
|
15
|
-
if (!raw)
|
|
16
|
-
return undefined;
|
|
17
|
-
try {
|
|
18
|
-
const parsed = JSON.parse(raw);
|
|
19
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
20
|
-
? parsed
|
|
21
|
-
: undefined;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function hasUnattemptedFailureSignature(issue, fallbackHeadSha) {
|
|
28
|
-
const signature = issue.lastGitHubFailureSignature;
|
|
29
|
-
if (!signature)
|
|
30
|
-
return false;
|
|
31
|
-
const headSha = issue.lastGitHubFailureHeadSha ?? fallbackHeadSha;
|
|
32
|
-
return issue.lastAttemptedFailureSignature !== signature
|
|
33
|
-
|| (headSha !== undefined && issue.lastAttemptedFailureHeadSha !== headSha);
|
|
34
|
-
}
|
|
35
|
-
function deriveImplicitReactiveWake(issue) {
|
|
36
|
-
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
37
|
-
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
38
|
-
activeRunId: issue.activeRunId,
|
|
39
|
-
prNumber: issue.prNumber,
|
|
40
|
-
prState: issue.prState,
|
|
41
|
-
prReviewState: issue.prReviewState,
|
|
42
|
-
prCheckStatus: issue.prCheckStatus,
|
|
43
|
-
latestFailureSource: issue.lastGitHubFailureSource,
|
|
44
|
-
});
|
|
45
|
-
if (!reactiveIntent)
|
|
46
|
-
return undefined;
|
|
47
|
-
if (reactiveIntent.runType === "ci_repair") {
|
|
48
|
-
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
|
|
49
|
-
const snapshot = parseObjectJson(issue.lastGitHubCiSnapshotJson);
|
|
50
|
-
const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
|
|
51
|
-
? failureContext.failureHeadSha
|
|
52
|
-
: issue.lastGitHubFailureHeadSha ?? issue.prHeadSha;
|
|
53
|
-
const failureSignature = issue.lastGitHubFailureSignature
|
|
54
|
-
?? (fallbackHeadSha ? `implicit_branch_ci::${fallbackHeadSha}` : undefined);
|
|
55
|
-
if (!failureSignature || issue.prState !== "open")
|
|
56
|
-
return undefined;
|
|
57
|
-
if (issue.lastAttemptedFailureSignature === failureSignature
|
|
58
|
-
&& (fallbackHeadSha === undefined || issue.lastAttemptedFailureHeadSha === fallbackHeadSha)) {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
return {
|
|
62
|
-
runType: reactiveIntent.runType,
|
|
63
|
-
wakeReason: reactiveIntent.wakeReason,
|
|
64
|
-
context: {
|
|
65
|
-
...failureContext,
|
|
66
|
-
failureSignature,
|
|
67
|
-
...(fallbackHeadSha ? { failureHeadSha: fallbackHeadSha } : {}),
|
|
68
|
-
...(issue.lastGitHubFailureCheckName ? { checkName: issue.lastGitHubFailureCheckName } : {}),
|
|
69
|
-
...(snapshot ? { ciSnapshot: snapshot } : {}),
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
if (reactiveIntent.runType === "queue_repair") {
|
|
74
|
-
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
|
|
75
|
-
const incidentContext = parseObjectJson(issue.lastQueueIncidentJson) ?? {};
|
|
76
|
-
const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
|
|
77
|
-
? failureContext.failureHeadSha
|
|
78
|
-
: undefined;
|
|
79
|
-
if (!hasUnattemptedFailureSignature(issue, fallbackHeadSha))
|
|
80
|
-
return undefined;
|
|
81
|
-
return {
|
|
82
|
-
runType: reactiveIntent.runType,
|
|
83
|
-
wakeReason: reactiveIntent.wakeReason,
|
|
84
|
-
context: {
|
|
85
|
-
...incidentContext,
|
|
86
|
-
...failureContext,
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
return undefined;
|
|
91
|
-
}
|
|
13
|
+
import { WorkflowWakeResolver } from "./workflow-wake-resolver.js";
|
|
92
14
|
export class PatchRelayDatabase {
|
|
93
15
|
connection;
|
|
94
16
|
linearInstallations;
|
|
@@ -97,6 +19,7 @@ export class PatchRelayDatabase {
|
|
|
97
19
|
webhookEvents;
|
|
98
20
|
issues;
|
|
99
21
|
issueSessions;
|
|
22
|
+
workflowWakes;
|
|
100
23
|
runs;
|
|
101
24
|
trackedIssues;
|
|
102
25
|
constructor(databasePath, wal) {
|
|
@@ -118,8 +41,9 @@ export class PatchRelayDatabase {
|
|
|
118
41
|
issue,
|
|
119
42
|
...(options ? { options } : {}),
|
|
120
43
|
}));
|
|
121
|
-
this.issueSessions = new IssueSessionStore(this.connection, mapIssueSessionRow, mapIssueSessionEventRow, this.issues, this.runs
|
|
122
|
-
this.
|
|
44
|
+
this.issueSessions = new IssueSessionStore(this.connection, mapIssueSessionRow, mapIssueSessionEventRow, this.issues, this.runs);
|
|
45
|
+
this.workflowWakes = new WorkflowWakeResolver(this.issues, this.issueSessions);
|
|
46
|
+
this.trackedIssues = new TrackedIssueQuery(this.issues, this.issueSessions, this.workflowWakes, this.runs);
|
|
123
47
|
}
|
|
124
48
|
runMigrations() {
|
|
125
49
|
runPatchRelayMigrations(this.connection);
|
|
@@ -513,7 +513,7 @@ export class IdleIssueReconciler {
|
|
|
513
513
|
}
|
|
514
514
|
if (issue.delegatedToPatchRelay
|
|
515
515
|
&& reactiveIntent?.runType === "review_fix"
|
|
516
|
-
&& this.db.
|
|
516
|
+
&& this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId) === undefined) {
|
|
517
517
|
this.logger.info({
|
|
518
518
|
issueKey: issue.issueKey,
|
|
519
519
|
prNumber: issue.prNumber,
|
|
@@ -147,7 +147,7 @@ export class InterruptedRunRecovery {
|
|
|
147
147
|
eventType: "delegated",
|
|
148
148
|
dedupeKey: `interrupted_implementation:implementation:${run.linearIssueId}`,
|
|
149
149
|
});
|
|
150
|
-
if (!this.db.
|
|
150
|
+
if (!this.db.workflowWakes.peekIssueWake(run.projectId, run.linearIssueId)) {
|
|
151
151
|
const failedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? refreshedIssue;
|
|
152
152
|
this.feed?.publish({
|
|
153
153
|
level: "error",
|
|
@@ -127,7 +127,7 @@ export class IssueOverviewQuery {
|
|
|
127
127
|
delegatedToPatchRelay: issueRecord?.delegatedToPatchRelay,
|
|
128
128
|
...(activeRun ? { activeRunId: activeRun.id } : {}),
|
|
129
129
|
blockedByCount: unresolvedBlockedBy.length,
|
|
130
|
-
hasPendingWake: this.db.
|
|
130
|
+
hasPendingWake: this.db.workflowWakes.peekIssueWake(session.projectId, session.linearIssueId) !== undefined,
|
|
131
131
|
hasLegacyPendingRun: issueRecord?.pendingRunType !== undefined,
|
|
132
132
|
orchestrationSettleUntil: issueRecord?.orchestrationSettleUntil,
|
|
133
133
|
...(session.prNumber !== undefined ? { prNumber: session.prNumber } : {}),
|
package/dist/run-wake-planner.js
CHANGED
|
@@ -5,7 +5,7 @@ export class RunWakePlanner {
|
|
|
5
5
|
this.db = db;
|
|
6
6
|
}
|
|
7
7
|
resolveRunWake(issue) {
|
|
8
|
-
const sessionWake = this.db.
|
|
8
|
+
const sessionWake = this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId);
|
|
9
9
|
if (!sessionWake)
|
|
10
10
|
return undefined;
|
|
11
11
|
return {
|
|
@@ -134,7 +134,7 @@ export class ServiceIssueActions {
|
|
|
134
134
|
status: "retry",
|
|
135
135
|
summary: `Retry queued: ${retryTarget.runType}`,
|
|
136
136
|
});
|
|
137
|
-
if (this.db.
|
|
137
|
+
if (this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId)) {
|
|
138
138
|
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
139
139
|
}
|
|
140
140
|
return { issueKey, runType: retryTarget.runType };
|
|
@@ -90,7 +90,7 @@ export class ServiceStartupRecovery {
|
|
|
90
90
|
}
|
|
91
91
|
const unresolvedBlockers = this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
|
|
92
92
|
const latestRun = this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
93
|
-
const hasPendingWake = this.db.
|
|
93
|
+
const hasPendingWake = this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId) !== undefined;
|
|
94
94
|
const shouldRecoverPausedLocalWork = delegated
|
|
95
95
|
&& isResumablePausedLocalWork({
|
|
96
96
|
issue: {
|
|
@@ -147,7 +147,7 @@ export class ServiceStartupRecovery {
|
|
|
147
147
|
dedupeKey: `delegated:${issue.linearIssueId}`,
|
|
148
148
|
});
|
|
149
149
|
}
|
|
150
|
-
if (this.db.
|
|
150
|
+
if (this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId)) {
|
|
151
151
|
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
152
152
|
}
|
|
153
153
|
this.logger.info({
|
|
@@ -155,7 +155,7 @@ export class TrackedIssueListQuery {
|
|
|
155
155
|
const blockedByCount = Number(row.blocked_by_count ?? 0);
|
|
156
156
|
const hasPendingSessionEvents = Number(row.pending_session_event_count ?? 0) > 0;
|
|
157
157
|
const hasPendingWake = hasPendingSessionEvents
|
|
158
|
-
|| this.db.
|
|
158
|
+
|| this.db.workflowWakes.peekIssueWake(String(row.project_id), String(row.linear_issue_id)) !== undefined;
|
|
159
159
|
const detachedActiveRun = hasDetachedActiveLatestRun({
|
|
160
160
|
activeRunId: row.active_run_type !== null ? 1 : undefined,
|
|
161
161
|
latestRun: row.latest_run_status !== null
|
|
@@ -4,10 +4,12 @@ import { resolveEffectiveActiveRun } from "./effective-active-run.js";
|
|
|
4
4
|
export class TrackedIssueQuery {
|
|
5
5
|
issues;
|
|
6
6
|
issueSessions;
|
|
7
|
+
workflowWakes;
|
|
7
8
|
runs;
|
|
8
|
-
constructor(issues, issueSessions, runs) {
|
|
9
|
+
constructor(issues, issueSessions, workflowWakes, runs) {
|
|
9
10
|
this.issues = issues;
|
|
10
11
|
this.issueSessions = issueSessions;
|
|
12
|
+
this.workflowWakes = workflowWakes;
|
|
11
13
|
this.runs = runs;
|
|
12
14
|
}
|
|
13
15
|
listIssuesReadyForExecution() {
|
|
@@ -20,7 +22,7 @@ export class TrackedIssueQuery {
|
|
|
20
22
|
}),
|
|
21
23
|
activeRunId: issue.activeRunId,
|
|
22
24
|
blockedByCount: this.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId),
|
|
23
|
-
hasPendingWake: this.
|
|
25
|
+
hasPendingWake: this.workflowWakes.hasPendingWake(issue.projectId, issue.linearIssueId),
|
|
24
26
|
hasLegacyPendingRun: issue.pendingRunType !== undefined,
|
|
25
27
|
prNumber: issue.prNumber,
|
|
26
28
|
prState: issue.prState,
|
|
@@ -38,7 +40,7 @@ export class TrackedIssueQuery {
|
|
|
38
40
|
issue,
|
|
39
41
|
session: this.issueSessions.getIssueSession(issue.projectId, issue.linearIssueId),
|
|
40
42
|
blockedBy: this.issues.listIssueDependencies(issue.projectId, issue.linearIssueId),
|
|
41
|
-
hasPendingWake: this.
|
|
43
|
+
hasPendingWake: this.workflowWakes.hasPendingWake(issue.projectId, issue.linearIssueId),
|
|
42
44
|
latestRun: this.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId),
|
|
43
45
|
latestEvent: this.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1),
|
|
44
46
|
});
|
package/dist/wake-dispatcher.js
CHANGED
|
@@ -69,7 +69,7 @@ export class WakeDispatcher {
|
|
|
69
69
|
const issue = this.db.issues.getIssue(projectId, linearIssueId);
|
|
70
70
|
if (issue?.activeRunId !== undefined)
|
|
71
71
|
return undefined;
|
|
72
|
-
const wake = this.db.
|
|
72
|
+
const wake = this.db.workflowWakes.peekIssueWake(projectId, linearIssueId);
|
|
73
73
|
// Fall back to the legacy pending_run_type column. The orchestrator
|
|
74
74
|
// materializes it into a real event at run time, but the poke still
|
|
75
75
|
// needs to happen now so the orchestrator gets called at all.
|
|
@@ -97,7 +97,7 @@ export class WakeDispatcher {
|
|
|
97
97
|
// check paths publish their own more-specific event and pass false.
|
|
98
98
|
releaseRunAndDispatch(params) {
|
|
99
99
|
this.releaseLease(params.run.projectId, params.run.linearIssueId);
|
|
100
|
-
const wake = this.db.
|
|
100
|
+
const wake = this.db.workflowWakes.peekIssueWake(params.run.projectId, params.run.linearIssueId);
|
|
101
101
|
if (!wake)
|
|
102
102
|
return undefined;
|
|
103
103
|
this.enqueueIssue(params.run.projectId, params.run.linearIssueId);
|
package/dist/webhook-handler.js
CHANGED
|
@@ -232,7 +232,7 @@ export class WebhookHandler {
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
peekPendingSessionWakeRunType(projectId, issueId) {
|
|
235
|
-
return this.db.
|
|
235
|
+
return this.db.workflowWakes.peekIssueWake(projectId, issueId)?.runType;
|
|
236
236
|
}
|
|
237
237
|
enqueuePendingSessionWake(projectId, issueId) {
|
|
238
238
|
return this.wakeDispatcher.dispatchIfWakePending(projectId, issueId);
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { computeOrchestrationSettleUntil, wakeOrchestrationParentsForChildEvent, } from "../orchestration-parent-wake.js";
|
|
1
|
+
import { wakeOrchestrationParentsForChildEvent } from "../orchestration-parent-wake.js";
|
|
3
2
|
import { triggerEventAllowed } from "../project-resolution.js";
|
|
4
|
-
import {
|
|
5
|
-
import { decideActiveRunRelease, decideAgentSession, decideRunIntent, decideUnDelegation, isResolvedLinearState, isTerminalDelegationState, resolveReDelegationResume, } from "./decision-helpers.js";
|
|
3
|
+
import { isResolvedLinearState } from "./decision-helpers.js";
|
|
6
4
|
import { isDelegatedToPatchRelay, resolveDelegationTruth } from "./delegation-truth.js";
|
|
7
5
|
import { syncIssueDependencies } from "./issue-dependency-sync.js";
|
|
8
6
|
import { resolveLinkedPrAdoption } from "./linked-pr-adoption.js";
|
|
9
7
|
import { buildOperatorRetryEvent } from "../operator-retry-event.js";
|
|
10
|
-
import {
|
|
8
|
+
import { planIssueWebhookWorkflow } from "./issue-webhook-workflow-planner.js";
|
|
11
9
|
export class DesiredStageRecorder {
|
|
12
10
|
db;
|
|
13
11
|
linearProvider;
|
|
@@ -29,7 +27,7 @@ export class DesiredStageRecorder {
|
|
|
29
27
|
const latestRun = existingIssue ? this.db.runs.getLatestRunForIssue(params.project.id, normalizedIssue.id) : undefined;
|
|
30
28
|
const triggerAllowed = triggerEventAllowed(params.project, params.normalized.triggerEvent);
|
|
31
29
|
const incomingAgentSessionId = params.normalized.agentSession?.id;
|
|
32
|
-
const hasPendingWake =
|
|
30
|
+
const hasPendingWake = params.peekPendingSessionWakeRunType(params.project.id, normalizedIssue.id) !== undefined;
|
|
33
31
|
if (!existingIssue && !isDelegatedToPatchRelay(this.db, params.project, normalizedIssue) && !incomingAgentSessionId) {
|
|
34
32
|
return { issue: undefined, wakeRunType: undefined, delegated: false };
|
|
35
33
|
}
|
|
@@ -56,108 +54,25 @@ export class DesiredStageRecorder {
|
|
|
56
54
|
triggerEvent: params.normalized.triggerEvent,
|
|
57
55
|
});
|
|
58
56
|
const unresolvedBlockers = this.db.issues.countUnresolvedBlockers(params.project.id, normalizedIssue.id);
|
|
59
|
-
const terminal = isTerminalDelegationState(existingIssue, hydratedIssue);
|
|
60
|
-
const openPrExists = existingIssue?.prNumber !== undefined
|
|
61
|
-
&& existingIssue.prState !== "closed"
|
|
62
|
-
&& existingIssue.prState !== "merged";
|
|
63
|
-
const blockerPausedImplementation = unresolvedBlockers > 0
|
|
64
|
-
&& activeRun?.runType === "implementation"
|
|
65
|
-
&& !openPrExists;
|
|
66
|
-
const desiredStage = linkedPrAdoption
|
|
67
|
-
? undefined
|
|
68
|
-
: decideRunIntent({
|
|
69
|
-
delegated,
|
|
70
|
-
triggerAllowed,
|
|
71
|
-
triggerEvent: params.normalized.triggerEvent,
|
|
72
|
-
unresolvedBlockers,
|
|
73
|
-
hasActiveRun: Boolean(activeRun),
|
|
74
|
-
hasPendingWake,
|
|
75
|
-
terminal,
|
|
76
|
-
currentState: existingIssue?.factoryState,
|
|
77
|
-
});
|
|
78
57
|
const childIssueCount = this.db.issues.listCanonicalChildIssues(params.project.id, normalizedIssue.id).length;
|
|
79
|
-
const classification = classifyIssue({
|
|
80
|
-
issue: {
|
|
81
|
-
issueClass: existingIssue?.issueClass,
|
|
82
|
-
issueClassSource: existingIssue?.issueClassSource,
|
|
83
|
-
title: hydratedIssue.title ?? existingIssue?.title,
|
|
84
|
-
description: hydratedIssue.description ?? existingIssue?.description,
|
|
85
|
-
parentLinearIssueId: hydratedIssue.parentId ?? existingIssue?.parentLinearIssueId,
|
|
86
|
-
},
|
|
87
|
-
childIssueCount,
|
|
88
|
-
});
|
|
89
|
-
const shouldEnterOrchestrationSettle = Boolean(delegated
|
|
90
|
-
&& desiredStage === "implementation"
|
|
91
|
-
&& classification.issueClass === "orchestration"
|
|
92
|
-
&& childIssueCount === 0
|
|
93
|
-
&& !existingIssue?.threadId
|
|
94
|
-
&& !activeRun
|
|
95
|
-
&& !terminal);
|
|
96
|
-
const runRelease = decideActiveRunRelease({
|
|
97
|
-
hasActiveRun: Boolean(activeRun),
|
|
98
|
-
terminal,
|
|
99
|
-
triggerEvent: params.normalized.triggerEvent,
|
|
100
|
-
delegated,
|
|
101
|
-
});
|
|
102
|
-
const effectiveRunRelease = blockerPausedImplementation
|
|
103
|
-
? { release: true, reason: "Issue became blocked during implementation" }
|
|
104
|
-
: runRelease;
|
|
105
|
-
const undelegation = decideUnDelegation({
|
|
106
|
-
triggerEvent: params.normalized.triggerEvent,
|
|
107
|
-
delegated,
|
|
108
|
-
currentState: existingIssue?.factoryState,
|
|
109
|
-
hasPr: existingIssue?.prNumber !== undefined && existingIssue?.prState !== "merged",
|
|
110
|
-
});
|
|
111
|
-
const startupResume = linkedPrAdoption
|
|
112
|
-
? {
|
|
113
|
-
factoryState: linkedPrAdoption.factoryState,
|
|
114
|
-
pendingRunType: linkedPrAdoption.pendingRunType,
|
|
115
|
-
pendingRunContext: linkedPrAdoption.pendingRunContext,
|
|
116
|
-
source: "linked_pr_adoption",
|
|
117
|
-
}
|
|
118
|
-
: {
|
|
119
|
-
...resolveReDelegationResume({
|
|
120
|
-
delegated,
|
|
121
|
-
previouslyDelegated: existingIssue?.delegatedToPatchRelay,
|
|
122
|
-
currentState: existingIssue?.factoryState,
|
|
123
|
-
awaitingInputReason: existingIssue
|
|
124
|
-
? resolveAwaitingInputReason({ issue: existingIssue, latestRun })
|
|
125
|
-
: undefined,
|
|
126
|
-
unresolvedBlockers,
|
|
127
|
-
prNumber: existingIssue?.prNumber,
|
|
128
|
-
prState: existingIssue?.prState,
|
|
129
|
-
prIsDraft: existingIssue?.prIsDraft,
|
|
130
|
-
prReviewState: existingIssue?.prReviewState,
|
|
131
|
-
prCheckStatus: existingIssue?.prCheckStatus,
|
|
132
|
-
latestFailureSource: existingIssue?.lastGitHubFailureSource,
|
|
133
|
-
}),
|
|
134
|
-
source: "re_delegated",
|
|
135
|
-
};
|
|
136
58
|
const existingWakeRunType = existingIssue
|
|
137
59
|
? params.peekPendingSessionWakeRunType(params.project.id, normalizedIssue.id)
|
|
138
60
|
: undefined;
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
triggerEvent: params.normalized.triggerEvent,
|
|
144
|
-
delegated,
|
|
145
|
-
});
|
|
146
|
-
const terminalRunRelease = effectiveRunRelease.release && terminal;
|
|
147
|
-
const resolvedPlan = resolveIssueUpdatePlan({
|
|
148
|
-
existingIssue: Boolean(existingIssue),
|
|
61
|
+
const workflowPlan = planIssueWebhookWorkflow({
|
|
62
|
+
existingIssue,
|
|
63
|
+
hydratedIssue,
|
|
64
|
+
latestRun,
|
|
149
65
|
delegated,
|
|
66
|
+
linkedPrAdoption,
|
|
67
|
+
triggerAllowed,
|
|
68
|
+
triggerEvent: params.normalized.triggerEvent,
|
|
69
|
+
unresolvedBlockers,
|
|
70
|
+
hasActiveRun: Boolean(activeRun),
|
|
71
|
+
activeRunType: activeRun?.runType,
|
|
72
|
+
hasPendingWake,
|
|
73
|
+
existingWakeRunType,
|
|
150
74
|
incomingAgentSessionId,
|
|
151
|
-
|
|
152
|
-
desiredStage,
|
|
153
|
-
terminalRunRelease,
|
|
154
|
-
blockerPausedImplementation,
|
|
155
|
-
undelegation,
|
|
156
|
-
clearPending,
|
|
157
|
-
effectiveRunRelease,
|
|
158
|
-
shouldEnterOrchestrationSettle,
|
|
159
|
-
agentSessionId,
|
|
160
|
-
computeOrchestrationSettleUntil,
|
|
75
|
+
childIssueCount,
|
|
161
76
|
});
|
|
162
77
|
const commitIssueUpdate = () => {
|
|
163
78
|
const record = this.db.issues.upsertIssue({
|
|
@@ -166,8 +81,8 @@ export class DesiredStageRecorder {
|
|
|
166
81
|
...(hydratedIssue.identifier ? { issueKey: hydratedIssue.identifier } : {}),
|
|
167
82
|
...(hydratedIssue.parentId !== undefined ? { parentLinearIssueId: hydratedIssue.parentId ?? null } : {}),
|
|
168
83
|
...(hydratedIssue.parentIdentifier !== undefined ? { parentIssueKey: hydratedIssue.parentIdentifier ?? null } : {}),
|
|
169
|
-
issueClass: classification.issueClass,
|
|
170
|
-
issueClassSource: classification.issueClassSource,
|
|
84
|
+
issueClass: workflowPlan.classification.issueClass,
|
|
85
|
+
issueClassSource: workflowPlan.classification.issueClassSource,
|
|
171
86
|
...(hydratedIssue.title ? { title: hydratedIssue.title } : {}),
|
|
172
87
|
...(hydratedIssue.description ? { description: hydratedIssue.description } : {}),
|
|
173
88
|
...(hydratedIssue.url ? { url: hydratedIssue.url } : {}),
|
|
@@ -177,10 +92,10 @@ export class DesiredStageRecorder {
|
|
|
177
92
|
...(hydratedIssue.stateType ? { currentLinearStateType: hydratedIssue.stateType } : {}),
|
|
178
93
|
...linkedPrAdoption?.issueUpdates,
|
|
179
94
|
delegatedToPatchRelay: delegated,
|
|
180
|
-
...
|
|
95
|
+
...workflowPlan.resolvedIssueUpdate,
|
|
181
96
|
});
|
|
182
|
-
if (effectiveRunRelease.release && activeRun && effectiveRunRelease.reason) {
|
|
183
|
-
this.db.runs.finishRun(activeRun.id, { status: "released", failureReason: effectiveRunRelease.reason });
|
|
97
|
+
if (workflowPlan.effectiveRunRelease.release && activeRun && workflowPlan.effectiveRunRelease.reason) {
|
|
98
|
+
this.db.runs.finishRun(activeRun.id, { status: "released", failureReason: workflowPlan.effectiveRunRelease.reason });
|
|
184
99
|
}
|
|
185
100
|
return record;
|
|
186
101
|
};
|
|
@@ -196,7 +111,7 @@ export class DesiredStageRecorder {
|
|
|
196
111
|
const currentParentIssueId = issue.parentLinearIssueId;
|
|
197
112
|
const wasResolved = isResolvedLinearState(existingIssue?.currentLinearStateType, existingIssue?.currentLinearState);
|
|
198
113
|
const isResolved = isResolvedLinearState(issue.currentLinearStateType, issue.currentLinearState);
|
|
199
|
-
if (undelegation.factoryState) {
|
|
114
|
+
if (workflowPlan.undelegation.factoryState) {
|
|
200
115
|
if (activeRun?.threadId && activeRun.turnId) {
|
|
201
116
|
await params.stopActiveRun(activeRun, "STOP: The issue was un-delegated from PatchRelay. Stop working immediately and exit.");
|
|
202
117
|
}
|
|
@@ -220,7 +135,7 @@ export class DesiredStageRecorder {
|
|
|
220
135
|
: `Issue un-delegated from PatchRelay; ${issue.factoryState} is now paused`,
|
|
221
136
|
});
|
|
222
137
|
}
|
|
223
|
-
else if (blockerPausedImplementation) {
|
|
138
|
+
else if (workflowPlan.blockerPausedImplementation) {
|
|
224
139
|
if (activeRun?.threadId && activeRun.turnId) {
|
|
225
140
|
await params.stopActiveRun(activeRun, "STOP: The issue is now blocked by another task. Stop working immediately and exit without publishing.");
|
|
226
141
|
}
|
|
@@ -236,14 +151,14 @@ export class DesiredStageRecorder {
|
|
|
236
151
|
summary: `Implementation paused because ${issue.issueKey ?? normalizedIssue.id} is now blocked`,
|
|
237
152
|
});
|
|
238
153
|
}
|
|
239
|
-
else if (startupResume.pendingRunType) {
|
|
154
|
+
else if (workflowPlan.startupResume.pendingRunType) {
|
|
240
155
|
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.project.id, normalizedIssue.id, {
|
|
241
156
|
projectId: params.project.id,
|
|
242
157
|
linearIssueId: normalizedIssue.id,
|
|
243
|
-
...buildOperatorRetryEvent(issue, startupResume.pendingRunType, startupResume.source),
|
|
158
|
+
...buildOperatorRetryEvent(issue, workflowPlan.startupResume.pendingRunType, workflowPlan.startupResume.source),
|
|
244
159
|
});
|
|
245
160
|
}
|
|
246
|
-
else if (shouldEnterOrchestrationSettle) {
|
|
161
|
+
else if (workflowPlan.shouldEnterOrchestrationSettle) {
|
|
247
162
|
this.feed?.publish({
|
|
248
163
|
level: "info",
|
|
249
164
|
kind: "stage",
|
|
@@ -254,10 +169,9 @@ export class DesiredStageRecorder {
|
|
|
254
169
|
summary: "Waiting briefly for child issues to settle before orchestration starts",
|
|
255
170
|
});
|
|
256
171
|
}
|
|
257
|
-
else if (!startupResume.factoryState
|
|
258
|
-
&& !startupResume.pendingRunType
|
|
259
|
-
&&
|
|
260
|
-
desiredStage === "implementation"
|
|
172
|
+
else if (!workflowPlan.startupResume.factoryState
|
|
173
|
+
&& !workflowPlan.startupResume.pendingRunType
|
|
174
|
+
&& workflowPlan.desiredStage === "implementation"
|
|
261
175
|
&& params.normalized.triggerEvent !== "commentCreated"
|
|
262
176
|
&& params.normalized.triggerEvent !== "commentUpdated"
|
|
263
177
|
&& params.normalized.triggerEvent !== "agentPrompted") {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { resolveAwaitingInputReason } from "../awaiting-input-reason.js";
|
|
2
|
+
import { computeOrchestrationSettleUntil as computeDefaultOrchestrationSettleUntil } from "../orchestration-parent-wake.js";
|
|
3
|
+
import { classifyIssue } from "../issue-class.js";
|
|
4
|
+
import { decideActiveRunRelease, decideAgentSession, decideRunIntent, decideUnDelegation, isTerminalDelegationState, resolveReDelegationResume, } from "./decision-helpers.js";
|
|
5
|
+
import { resolveIssueUpdatePlan } from "./issue-update-plan.js";
|
|
6
|
+
function resolveStartupResume(input) {
|
|
7
|
+
if (input.linkedPrAdoption) {
|
|
8
|
+
return {
|
|
9
|
+
factoryState: input.linkedPrAdoption.factoryState,
|
|
10
|
+
pendingRunType: input.linkedPrAdoption.pendingRunType,
|
|
11
|
+
pendingRunContext: input.linkedPrAdoption.pendingRunContext,
|
|
12
|
+
source: "linked_pr_adoption",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const awaitingInputReason = input.existingIssue
|
|
16
|
+
? resolveAwaitingInputReason({ issue: input.existingIssue, latestRun: input.latestRun })
|
|
17
|
+
: undefined;
|
|
18
|
+
return {
|
|
19
|
+
...resolveReDelegationResume({
|
|
20
|
+
delegated: input.delegated,
|
|
21
|
+
previouslyDelegated: input.existingIssue?.delegatedToPatchRelay,
|
|
22
|
+
currentState: input.existingIssue?.factoryState,
|
|
23
|
+
awaitingInputReason,
|
|
24
|
+
unresolvedBlockers: input.unresolvedBlockers,
|
|
25
|
+
prNumber: input.existingIssue?.prNumber,
|
|
26
|
+
prState: input.existingIssue?.prState,
|
|
27
|
+
prIsDraft: input.existingIssue?.prIsDraft,
|
|
28
|
+
prReviewState: input.existingIssue?.prReviewState,
|
|
29
|
+
prCheckStatus: input.existingIssue?.prCheckStatus,
|
|
30
|
+
latestFailureSource: input.existingIssue?.lastGitHubFailureSource,
|
|
31
|
+
}),
|
|
32
|
+
source: "re_delegated",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function planIssueWebhookWorkflow(input) {
|
|
36
|
+
const terminal = isTerminalDelegationState(input.existingIssue, input.hydratedIssue);
|
|
37
|
+
const openPrExists = input.existingIssue?.prNumber !== undefined
|
|
38
|
+
&& input.existingIssue.prState !== "closed"
|
|
39
|
+
&& input.existingIssue.prState !== "merged";
|
|
40
|
+
const blockerPausedImplementation = input.unresolvedBlockers > 0
|
|
41
|
+
&& input.activeRunType === "implementation"
|
|
42
|
+
&& !openPrExists;
|
|
43
|
+
const desiredStage = input.linkedPrAdoption
|
|
44
|
+
? undefined
|
|
45
|
+
: decideRunIntent({
|
|
46
|
+
delegated: input.delegated,
|
|
47
|
+
triggerAllowed: input.triggerAllowed,
|
|
48
|
+
triggerEvent: input.triggerEvent,
|
|
49
|
+
unresolvedBlockers: input.unresolvedBlockers,
|
|
50
|
+
hasActiveRun: input.hasActiveRun,
|
|
51
|
+
hasPendingWake: input.hasPendingWake,
|
|
52
|
+
terminal,
|
|
53
|
+
currentState: input.existingIssue?.factoryState,
|
|
54
|
+
});
|
|
55
|
+
const classification = classifyIssue({
|
|
56
|
+
issue: {
|
|
57
|
+
issueClass: input.existingIssue?.issueClass,
|
|
58
|
+
issueClassSource: input.existingIssue?.issueClassSource,
|
|
59
|
+
title: input.hydratedIssue.title ?? input.existingIssue?.title,
|
|
60
|
+
description: input.hydratedIssue.description ?? input.existingIssue?.description,
|
|
61
|
+
parentLinearIssueId: input.hydratedIssue.parentId ?? input.existingIssue?.parentLinearIssueId,
|
|
62
|
+
},
|
|
63
|
+
childIssueCount: input.childIssueCount,
|
|
64
|
+
});
|
|
65
|
+
const shouldEnterOrchestrationSettle = Boolean(input.delegated
|
|
66
|
+
&& desiredStage === "implementation"
|
|
67
|
+
&& classification.issueClass === "orchestration"
|
|
68
|
+
&& input.childIssueCount === 0
|
|
69
|
+
&& !input.existingIssue?.threadId
|
|
70
|
+
&& !input.hasActiveRun
|
|
71
|
+
&& !terminal);
|
|
72
|
+
const runRelease = decideActiveRunRelease({
|
|
73
|
+
hasActiveRun: input.hasActiveRun,
|
|
74
|
+
terminal,
|
|
75
|
+
triggerEvent: input.triggerEvent,
|
|
76
|
+
delegated: input.delegated,
|
|
77
|
+
});
|
|
78
|
+
const effectiveRunRelease = blockerPausedImplementation
|
|
79
|
+
? { release: true, reason: "Issue became blocked during implementation" }
|
|
80
|
+
: runRelease;
|
|
81
|
+
const undelegation = decideUnDelegation({
|
|
82
|
+
triggerEvent: input.triggerEvent,
|
|
83
|
+
delegated: input.delegated,
|
|
84
|
+
currentState: input.existingIssue?.factoryState,
|
|
85
|
+
hasPr: input.existingIssue?.prNumber !== undefined && input.existingIssue?.prState !== "merged",
|
|
86
|
+
});
|
|
87
|
+
const startupResume = resolveStartupResume(input);
|
|
88
|
+
const clearPending = (input.unresolvedBlockers > 0 && input.existingWakeRunType === "implementation" && !input.hasActiveRun)
|
|
89
|
+
|| undelegation.clearPending;
|
|
90
|
+
const agentSessionId = decideAgentSession({
|
|
91
|
+
sessionId: input.incomingAgentSessionId,
|
|
92
|
+
triggerEvent: input.triggerEvent,
|
|
93
|
+
delegated: input.delegated,
|
|
94
|
+
});
|
|
95
|
+
const terminalRunRelease = effectiveRunRelease.release && terminal;
|
|
96
|
+
const resolvedIssueUpdate = resolveIssueUpdatePlan({
|
|
97
|
+
existingIssue: Boolean(input.existingIssue),
|
|
98
|
+
delegated: input.delegated,
|
|
99
|
+
incomingAgentSessionId: input.incomingAgentSessionId,
|
|
100
|
+
startupResume,
|
|
101
|
+
desiredStage,
|
|
102
|
+
terminalRunRelease,
|
|
103
|
+
blockerPausedImplementation,
|
|
104
|
+
undelegation,
|
|
105
|
+
clearPending,
|
|
106
|
+
effectiveRunRelease,
|
|
107
|
+
shouldEnterOrchestrationSettle,
|
|
108
|
+
agentSessionId,
|
|
109
|
+
computeOrchestrationSettleUntil: input.computeOrchestrationSettleUntil ?? computeDefaultOrchestrationSettleUntil,
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
classification,
|
|
113
|
+
terminal,
|
|
114
|
+
desiredStage,
|
|
115
|
+
blockerPausedImplementation,
|
|
116
|
+
undelegation,
|
|
117
|
+
startupResume,
|
|
118
|
+
effectiveRunRelease,
|
|
119
|
+
clearPending,
|
|
120
|
+
agentSessionId,
|
|
121
|
+
shouldEnterOrchestrationSettle,
|
|
122
|
+
resolvedIssueUpdate,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
2
|
+
function parseObjectJson(raw) {
|
|
3
|
+
if (!raw)
|
|
4
|
+
return undefined;
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(raw);
|
|
7
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
8
|
+
? parsed
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function hasUnattemptedFailureSignature(issue, fallbackHeadSha) {
|
|
16
|
+
const signature = issue.lastGitHubFailureSignature;
|
|
17
|
+
if (!signature)
|
|
18
|
+
return false;
|
|
19
|
+
const headSha = issue.lastGitHubFailureHeadSha ?? fallbackHeadSha;
|
|
20
|
+
return issue.lastAttemptedFailureSignature !== signature
|
|
21
|
+
|| (headSha !== undefined && issue.lastAttemptedFailureHeadSha !== headSha);
|
|
22
|
+
}
|
|
23
|
+
export function deriveImplicitReactiveWake(issue) {
|
|
24
|
+
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
25
|
+
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
26
|
+
activeRunId: issue.activeRunId,
|
|
27
|
+
prNumber: issue.prNumber,
|
|
28
|
+
prState: issue.prState,
|
|
29
|
+
prReviewState: issue.prReviewState,
|
|
30
|
+
prCheckStatus: issue.prCheckStatus,
|
|
31
|
+
latestFailureSource: issue.lastGitHubFailureSource,
|
|
32
|
+
});
|
|
33
|
+
if (!reactiveIntent)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (reactiveIntent.runType === "ci_repair") {
|
|
36
|
+
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
|
|
37
|
+
const snapshot = parseObjectJson(issue.lastGitHubCiSnapshotJson);
|
|
38
|
+
const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
|
|
39
|
+
? failureContext.failureHeadSha
|
|
40
|
+
: issue.lastGitHubFailureHeadSha ?? issue.prHeadSha;
|
|
41
|
+
const failureSignature = issue.lastGitHubFailureSignature
|
|
42
|
+
?? (fallbackHeadSha ? `implicit_branch_ci::${fallbackHeadSha}` : undefined);
|
|
43
|
+
if (!failureSignature || issue.prState !== "open")
|
|
44
|
+
return undefined;
|
|
45
|
+
if (issue.lastAttemptedFailureSignature === failureSignature
|
|
46
|
+
&& (fallbackHeadSha === undefined || issue.lastAttemptedFailureHeadSha === fallbackHeadSha)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
runType: reactiveIntent.runType,
|
|
51
|
+
wakeReason: reactiveIntent.wakeReason,
|
|
52
|
+
context: {
|
|
53
|
+
...failureContext,
|
|
54
|
+
failureSignature,
|
|
55
|
+
...(fallbackHeadSha ? { failureHeadSha: fallbackHeadSha } : {}),
|
|
56
|
+
...(issue.lastGitHubFailureCheckName ? { checkName: issue.lastGitHubFailureCheckName } : {}),
|
|
57
|
+
...(snapshot ? { ciSnapshot: snapshot } : {}),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (reactiveIntent.runType === "queue_repair") {
|
|
62
|
+
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
|
|
63
|
+
const incidentContext = parseObjectJson(issue.lastQueueIncidentJson) ?? {};
|
|
64
|
+
const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
|
|
65
|
+
? failureContext.failureHeadSha
|
|
66
|
+
: undefined;
|
|
67
|
+
if (!hasUnattemptedFailureSignature(issue, fallbackHeadSha))
|
|
68
|
+
return undefined;
|
|
69
|
+
return {
|
|
70
|
+
runType: reactiveIntent.runType,
|
|
71
|
+
wakeReason: reactiveIntent.wakeReason,
|
|
72
|
+
context: {
|
|
73
|
+
...incidentContext,
|
|
74
|
+
...failureContext,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
export class WorkflowWakeResolver {
|
|
81
|
+
issues;
|
|
82
|
+
issueSessions;
|
|
83
|
+
constructor(issues, issueSessions) {
|
|
84
|
+
this.issues = issues;
|
|
85
|
+
this.issueSessions = issueSessions;
|
|
86
|
+
}
|
|
87
|
+
peekIssueWake(projectId, linearIssueId) {
|
|
88
|
+
const explicitWake = this.issueSessions.peekIssueSessionWake(projectId, linearIssueId);
|
|
89
|
+
if (explicitWake)
|
|
90
|
+
return explicitWake;
|
|
91
|
+
const issue = this.issues.getIssue(projectId, linearIssueId);
|
|
92
|
+
if (!issue)
|
|
93
|
+
return undefined;
|
|
94
|
+
const implicitWake = deriveImplicitReactiveWake(issue);
|
|
95
|
+
if (!implicitWake)
|
|
96
|
+
return undefined;
|
|
97
|
+
return {
|
|
98
|
+
eventIds: [],
|
|
99
|
+
runType: implicitWake.runType,
|
|
100
|
+
context: implicitWake.context,
|
|
101
|
+
wakeReason: implicitWake.wakeReason,
|
|
102
|
+
resumeThread: false,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
hasPendingWake(projectId, linearIssueId) {
|
|
106
|
+
return this.peekIssueWake(projectId, linearIssueId) !== undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|