patchrelay 0.83.0 → 0.83.1
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.js +15 -0
- package/dist/linear-progress-reporter.js +15 -2
- package/dist/orchestration-parent-wake.js +11 -0
- package/dist/run-notification-handler.js +16 -1
- package/dist/run-wake-planner.js +68 -6
- package/dist/service-runtime.js +30 -2
- package/dist/service.js +3 -0
- package/dist/sqlite-errors.js +5 -0
- package/dist/wake-dispatcher.js +99 -13
- package/dist/workflow-runtime.js +3 -0
- package/dist/workflow-task-reconciler.js +18 -10
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/db.js
CHANGED
|
@@ -87,6 +87,21 @@ export class PatchRelayDatabase {
|
|
|
87
87
|
assertSchemaReady() {
|
|
88
88
|
assertPatchRelaySchemaReady(this.connection, this.databasePath);
|
|
89
89
|
}
|
|
90
|
+
describeSchema() {
|
|
91
|
+
const tableRows = this.connection.prepare(`
|
|
92
|
+
SELECT name FROM sqlite_master
|
|
93
|
+
WHERE type = 'table' AND name IN ('issues', 'issue_sessions', 'runs')
|
|
94
|
+
ORDER BY name
|
|
95
|
+
`).all();
|
|
96
|
+
const issueColumns = tableRows.some((row) => row.name === "issues")
|
|
97
|
+
? this.connection.prepare("PRAGMA table_info(issues)").all().map((row) => row.name)
|
|
98
|
+
: [];
|
|
99
|
+
return {
|
|
100
|
+
databasePath: this.databasePath,
|
|
101
|
+
tables: tableRows.map((row) => row.name),
|
|
102
|
+
issuesVersionColumnPresent: issueColumns.includes("version"),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
90
105
|
transaction(fn) {
|
|
91
106
|
return this.connection.transaction(fn)();
|
|
92
107
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { deriveLinearProgressFact } from "./linear-progress-facts.js";
|
|
2
|
+
import { isSqliteSchemaReadError } from "./sqlite-errors.js";
|
|
2
3
|
export class LinearProgressReporter {
|
|
3
4
|
db;
|
|
4
5
|
emitActivity;
|
|
@@ -11,7 +12,7 @@ export class LinearProgressReporter {
|
|
|
11
12
|
this.options = options;
|
|
12
13
|
}
|
|
13
14
|
maybeEmitProgress(notification, run) {
|
|
14
|
-
const issue = this.
|
|
15
|
+
const issue = this.getIssueWithSchemaRetry(run);
|
|
15
16
|
if (!issue) {
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
@@ -73,7 +74,7 @@ export class LinearProgressReporter {
|
|
|
73
74
|
if (previous?.lastHeartbeatAtMs !== undefined && now - previous.lastHeartbeatAtMs < intervalMs) {
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
76
|
-
const issue = this.
|
|
77
|
+
const issue = this.getIssueWithSchemaRetry(run);
|
|
77
78
|
if (!issue) {
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
@@ -108,6 +109,18 @@ export class LinearProgressReporter {
|
|
|
108
109
|
now() {
|
|
109
110
|
return this.options.now?.() ?? Date.now();
|
|
110
111
|
}
|
|
112
|
+
getIssueWithSchemaRetry(run) {
|
|
113
|
+
try {
|
|
114
|
+
return this.db.getIssue(run.projectId, run.linearIssueId);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (!isSqliteSchemaReadError(error)) {
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
this.db.assertSchemaReady();
|
|
121
|
+
return this.db.getIssue(run.projectId, run.linearIssueId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
111
124
|
clearFailedPublication(runId, channel, meaningKey, publishedAtMs) {
|
|
112
125
|
const current = this.publicationsByRun.get(runId);
|
|
113
126
|
if (!current) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { classifyIssue } from "./issue-class.js";
|
|
2
|
+
import { reconcileWorkflowTasksForIssue } from "./workflow-task-reconciler.js";
|
|
2
3
|
const WRITER = "orchestration-parent-wake";
|
|
3
4
|
export const ORCHESTRATION_SETTLE_WINDOW_MS = 10_000;
|
|
4
5
|
export function computeOrchestrationSettleUntil(now = Date.now()) {
|
|
@@ -23,6 +24,12 @@ function resolveParentIssueIds(db, child) {
|
|
|
23
24
|
}
|
|
24
25
|
return unique(parentIds);
|
|
25
26
|
}
|
|
27
|
+
function parentHasRunnableWorkflowTask(db, parent) {
|
|
28
|
+
const reconciliation = reconcileWorkflowTasksForIssue(db, parent);
|
|
29
|
+
return reconciliation.result.open.some((task) => (task.taskType === "run"
|
|
30
|
+
&& task.runType !== undefined
|
|
31
|
+
&& task.gateAction === "start"));
|
|
32
|
+
}
|
|
26
33
|
export function startOrchestrationSettleWindow(db, issue, now = Date.now()) {
|
|
27
34
|
const settleUntil = computeOrchestrationSettleUntil(now);
|
|
28
35
|
db.issueSessions.commitIssueState({
|
|
@@ -70,6 +77,10 @@ export function wakeOrchestrationParentsForChildEvent(params) {
|
|
|
70
77
|
parentIds.push(parent.linearIssueId);
|
|
71
78
|
continue;
|
|
72
79
|
}
|
|
80
|
+
if (!parentHasRunnableWorkflowTask(params.db, parent)) {
|
|
81
|
+
parentIds.push(parent.linearIssueId);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
73
84
|
params.wakeDispatcher.recordEventAndDispatch(parent.projectId, parent.linearIssueId, {
|
|
74
85
|
eventType: params.eventType,
|
|
75
86
|
eventJson: JSON.stringify({
|
|
@@ -234,7 +234,14 @@ export class RunNotificationHandler {
|
|
|
234
234
|
this.linearSync.maybeEmitProgress(notification, run);
|
|
235
235
|
}
|
|
236
236
|
catch (error) {
|
|
237
|
-
this.logger.warn({
|
|
237
|
+
this.logger.warn({
|
|
238
|
+
runId: run.id,
|
|
239
|
+
projectId: run.projectId,
|
|
240
|
+
issueId: run.linearIssueId,
|
|
241
|
+
method: notification.method,
|
|
242
|
+
error: formatError(error),
|
|
243
|
+
storage: this.safeStorageDiagnostics(),
|
|
244
|
+
}, "Linear progress reporting failed");
|
|
238
245
|
}
|
|
239
246
|
}
|
|
240
247
|
syncCodexPlan(notification, run) {
|
|
@@ -257,6 +264,14 @@ export class RunNotificationHandler {
|
|
|
257
264
|
this.logger.warn({ runId: run.id, issueKey: issue.issueKey, projectId: run.projectId, issueId: run.linearIssueId, method: notification.method, error: formatError(error) }, "Linear plan sync failed");
|
|
258
265
|
}
|
|
259
266
|
}
|
|
267
|
+
safeStorageDiagnostics() {
|
|
268
|
+
try {
|
|
269
|
+
return this.db.describeSchema();
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
260
275
|
}
|
|
261
276
|
function formatError(error) {
|
|
262
277
|
return error instanceof Error ? error.message : String(error);
|
package/dist/run-wake-planner.js
CHANGED
|
@@ -2,6 +2,7 @@ import { getCiRepairBudget, getQueueRepairBudget, getReviewFixBudget, } from "./
|
|
|
2
2
|
import { buildRequestedChangesWakeIdentity } from "./reactive-wake-keys.js";
|
|
3
3
|
import { parseRunContextOrWarn, serializeRunContext, tryParseRunContextValue } from "./run-context.js";
|
|
4
4
|
import { assertNever } from "./utils.js";
|
|
5
|
+
import { reconcileWorkflowTasksForIssue } from "./workflow-task-reconciler.js";
|
|
5
6
|
const WRITER = "run-wake-planner";
|
|
6
7
|
function parseObjectJson(raw) {
|
|
7
8
|
if (!raw)
|
|
@@ -24,11 +25,22 @@ export class RunWakePlanner {
|
|
|
24
25
|
this.logger = logger;
|
|
25
26
|
}
|
|
26
27
|
resolveRunWake(issue) {
|
|
27
|
-
|
|
28
|
+
const freshIssue = this.db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
29
|
+
if (this.db.issues.countUnresolvedBlockers(freshIssue.projectId, freshIssue.linearIssueId) > 0) {
|
|
28
30
|
return undefined;
|
|
29
31
|
}
|
|
30
|
-
const
|
|
32
|
+
const existingWorkflowTaskWake = this.resolveWorkflowTaskWake(freshIssue);
|
|
33
|
+
if (existingWorkflowTaskWake)
|
|
34
|
+
return existingWorkflowTaskWake;
|
|
35
|
+
this.reconcileWorkflowTasks(freshIssue);
|
|
36
|
+
const workflowTaskWake = this.resolveWorkflowTaskWake(freshIssue);
|
|
37
|
+
if (workflowTaskWake)
|
|
38
|
+
return workflowTaskWake;
|
|
39
|
+
const sessionWake = this.db.issueSessions.peekIssueSessionWake(freshIssue.projectId, freshIssue.linearIssueId);
|
|
31
40
|
if (sessionWake) {
|
|
41
|
+
if (this.workflowTasksSuppressSessionWake(freshIssue, sessionWake.wakeReason)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
32
44
|
return {
|
|
33
45
|
runType: sessionWake.runType,
|
|
34
46
|
context: sessionWake.context,
|
|
@@ -37,10 +49,10 @@ export class RunWakePlanner {
|
|
|
37
49
|
eventIds: sessionWake.eventIds,
|
|
38
50
|
};
|
|
39
51
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const implicitWake = this.db.workflowWakes.peekIssueWake(
|
|
52
|
+
if (this.workflowTasksSuppressSessionWake(freshIssue, undefined)) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const implicitWake = this.db.workflowWakes.peekIssueWake(freshIssue.projectId, freshIssue.linearIssueId);
|
|
44
56
|
if (!implicitWake)
|
|
45
57
|
return undefined;
|
|
46
58
|
return {
|
|
@@ -72,6 +84,56 @@ export class RunWakePlanner {
|
|
|
72
84
|
eventIds: [],
|
|
73
85
|
};
|
|
74
86
|
}
|
|
87
|
+
reconcileWorkflowTasks(issue) {
|
|
88
|
+
try {
|
|
89
|
+
reconcileWorkflowTasksForIssue(this.db, issue);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
this.logger?.warn({
|
|
93
|
+
projectId: issue.projectId,
|
|
94
|
+
linearIssueId: issue.linearIssueId,
|
|
95
|
+
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
96
|
+
error: error instanceof Error ? error.message : String(error),
|
|
97
|
+
}, "Workflow task reconciliation failed while planning run wake");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
workflowTasksSuppressSessionWake(issue, wakeReason) {
|
|
101
|
+
const openTasks = this.db.workflowTasks.listOpenTasks(issue.projectId, issue.linearIssueId);
|
|
102
|
+
if (openTasks.length === 0)
|
|
103
|
+
return false;
|
|
104
|
+
if (openTasks.some((task) => task.taskType === "run" && task.gateAction === "start" && task.runType !== undefined)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (!openTasks.some((task) => this.isBlockingWorkflowGate(task)))
|
|
108
|
+
return false;
|
|
109
|
+
if (!openTasks.every((task) => task.taskId === "wait:input")) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
return wakeReason !== "direct_reply"
|
|
113
|
+
&& wakeReason !== "followup_prompt"
|
|
114
|
+
&& wakeReason !== "followup_comment"
|
|
115
|
+
&& wakeReason !== "human_instruction"
|
|
116
|
+
&& wakeReason !== "operator_prompt"
|
|
117
|
+
&& wakeReason !== "completion_check_continue";
|
|
118
|
+
}
|
|
119
|
+
isBlockingWorkflowGate(task) {
|
|
120
|
+
if (task.taskId === "wait:input")
|
|
121
|
+
return true;
|
|
122
|
+
if (task.taskId === "wait:children" || task.taskId === "wait:blockers" || task.taskId.startsWith("wait:active-run:")) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (task.taskId === "wait:authority") {
|
|
126
|
+
return this.workflowAuthorityObserved(task.projectId, task.subjectId);
|
|
127
|
+
}
|
|
128
|
+
return task.taskType === "verify" || task.taskType === "ask" || task.taskType === "escalate" || task.taskType === "publish";
|
|
129
|
+
}
|
|
130
|
+
workflowAuthorityObserved(projectId, linearIssueId) {
|
|
131
|
+
return this.db.workflowObservations
|
|
132
|
+
.listObservations(projectId, linearIssueId)
|
|
133
|
+
.some((observation) => (observation.type === "linear.delegated"
|
|
134
|
+
|| observation.type === "linear.undelegated"
|
|
135
|
+
|| observation.type === "operator.authority_changed"));
|
|
136
|
+
}
|
|
75
137
|
appendWakeEventWithLease(lease, issue, runType, context, dedupeScope) {
|
|
76
138
|
let eventType;
|
|
77
139
|
let dedupeKey;
|
package/dist/service-runtime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SerialWorkQueue } from "./service-queue.js";
|
|
2
2
|
import { retrySqliteLockedQueueFailure } from "./queue-failure-policy.js";
|
|
3
|
+
import { isSqliteSchemaReadError } from "./sqlite-errors.js";
|
|
3
4
|
const ISSUE_KEY_DELIMITER = "::";
|
|
4
5
|
const DEFAULT_RECONCILE_INTERVAL_MS = 5_000;
|
|
5
6
|
const DEFAULT_RECONCILE_TIMEOUT_MS = 60_000;
|
|
@@ -135,7 +136,7 @@ export class ServiceRuntime {
|
|
|
135
136
|
}
|
|
136
137
|
this.reconcileInProgress = true;
|
|
137
138
|
try {
|
|
138
|
-
await
|
|
139
|
+
await this.reconcileActiveRunsWithSchemaRetry();
|
|
139
140
|
// Pick up issues that became ready outside the webhook path
|
|
140
141
|
// (e.g. CLI retry, manual DB edits) without requiring a restart.
|
|
141
142
|
for (const issue of this.readyIssueSource.listIssuesReadyForExecution()) {
|
|
@@ -143,7 +144,10 @@ export class ServiceRuntime {
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
catch (error) {
|
|
146
|
-
this.logger.warn({
|
|
147
|
+
this.logger.warn({
|
|
148
|
+
error: error instanceof Error ? error.message : String(error),
|
|
149
|
+
storage: this.safeStorageDiagnostics(),
|
|
150
|
+
}, "Background active-run reconciliation failed");
|
|
147
151
|
}
|
|
148
152
|
finally {
|
|
149
153
|
this.reconcileInProgress = false;
|
|
@@ -152,6 +156,30 @@ export class ServiceRuntime {
|
|
|
152
156
|
}
|
|
153
157
|
}
|
|
154
158
|
}
|
|
159
|
+
async reconcileActiveRunsWithSchemaRetry() {
|
|
160
|
+
try {
|
|
161
|
+
await this.reconcileActiveRunsOnce();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (!isSqliteSchemaReadError(error) || !this.options.assertStorageReady) {
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
this.options.assertStorageReady();
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
169
|
+
await this.reconcileActiveRunsOnce();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async reconcileActiveRunsOnce() {
|
|
173
|
+
await promiseWithTimeout(this.runReconciler.reconcileActiveRuns(), this.options.reconcileTimeoutMs ?? DEFAULT_RECONCILE_TIMEOUT_MS, "Background active-run reconciliation");
|
|
174
|
+
}
|
|
175
|
+
safeStorageDiagnostics() {
|
|
176
|
+
try {
|
|
177
|
+
return this.options.describeStorage?.();
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
155
183
|
getMaxActiveIssueRuns() {
|
|
156
184
|
const configured = this.options.maxActiveIssueRuns ?? DEFAULT_MAX_ACTIVE_ISSUE_RUNS;
|
|
157
185
|
return Math.max(1, Math.floor(configured));
|
package/dist/service.js
CHANGED
|
@@ -77,6 +77,9 @@ export class PatchRelayService {
|
|
|
77
77
|
processIssue: async (item) => {
|
|
78
78
|
await this.orchestrator.run(item);
|
|
79
79
|
},
|
|
80
|
+
}, {
|
|
81
|
+
assertStorageReady: () => db.assertSchemaReady(),
|
|
82
|
+
describeStorage: () => db.describeSchema(),
|
|
80
83
|
});
|
|
81
84
|
enqueueIssue = (projectId, issueId) => runtime.enqueueIssue(projectId, issueId);
|
|
82
85
|
this.oauthService = new LinearOAuthService(config, { linearInstallations: db.linearInstallations }, logger);
|
package/dist/wake-dispatcher.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { emitTelemetry, noopTelemetry } from "./telemetry.js";
|
|
2
|
+
import { reconcileWorkflowTasksForIssue } from "./workflow-task-reconciler.js";
|
|
2
3
|
// Single owner of "append a session event and tell the orchestrator
|
|
3
4
|
// something might be runnable", and of "release a finished run so the
|
|
4
5
|
// next wake fires." Until this existed, 8+ call sites each made their
|
|
@@ -32,22 +33,82 @@ export class WakeDispatcher {
|
|
|
32
33
|
this.feed = feed;
|
|
33
34
|
this.telemetry = telemetry;
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
-
return this.db.workflowTasks
|
|
37
|
-
.listOpenRunnableTasks(projectId)
|
|
38
|
-
.find((task) => task.subjectId === linearIssueId && task.runType !== undefined);
|
|
36
|
+
listOpenWorkflowTasks(projectId, linearIssueId) {
|
|
37
|
+
return this.db.workflowTasks.listOpenTasks(projectId, linearIssueId);
|
|
39
38
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
reconcileOpenWorkflowTasks(issue, options) {
|
|
40
|
+
try {
|
|
41
|
+
return reconcileWorkflowTasksForIssue(this.db, issue, options).result.open;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
this.logger.warn({
|
|
45
|
+
projectId: issue.projectId,
|
|
46
|
+
linearIssueId: issue.linearIssueId,
|
|
47
|
+
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
48
|
+
error: error instanceof Error ? error.message : String(error),
|
|
49
|
+
}, "Workflow task reconciliation failed while resolving wake");
|
|
50
|
+
return this.listOpenWorkflowTasks(issue.projectId, issue.linearIssueId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
peekRunnableWorkflowTask(projectId, linearIssueId, openTasks) {
|
|
54
|
+
return (openTasks ?? this.db.workflowTasks.listOpenRunnableTasks(projectId))
|
|
55
|
+
.find((task) => (task.subjectId === linearIssueId
|
|
56
|
+
&& task.taskType === "run"
|
|
57
|
+
&& task.gateAction === "start"
|
|
58
|
+
&& task.runType !== undefined));
|
|
59
|
+
}
|
|
60
|
+
workflowAuthorityObserved(projectId, linearIssueId) {
|
|
61
|
+
return this.db.workflowObservations
|
|
62
|
+
.listObservations(projectId, linearIssueId)
|
|
63
|
+
.some((observation) => (observation.type === "linear.delegated"
|
|
64
|
+
|| observation.type === "linear.undelegated"
|
|
65
|
+
|| observation.type === "operator.authority_changed"));
|
|
66
|
+
}
|
|
67
|
+
sessionWakeCanAnswerInputWait(openTasks, wakeReason) {
|
|
68
|
+
if (openTasks.length === 0 || !openTasks.every((task) => task.taskId === "wait:input")) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return wakeReason === "direct_reply"
|
|
72
|
+
|| wakeReason === "followup_prompt"
|
|
73
|
+
|| wakeReason === "followup_comment"
|
|
74
|
+
|| wakeReason === "human_instruction"
|
|
75
|
+
|| wakeReason === "operator_prompt"
|
|
76
|
+
|| wakeReason === "completion_check_continue";
|
|
77
|
+
}
|
|
78
|
+
workflowTasksSuppressSessionWake(openTasks, wakeReason) {
|
|
79
|
+
if (openTasks.length === 0)
|
|
80
|
+
return false;
|
|
81
|
+
if (this.peekRunnableWorkflowTask(openTasks[0].projectId, openTasks[0].subjectId, openTasks))
|
|
82
|
+
return false;
|
|
83
|
+
if (!openTasks.some((task) => this.isBlockingWorkflowGate(task)))
|
|
84
|
+
return false;
|
|
85
|
+
return !this.sessionWakeCanAnswerInputWait(openTasks, wakeReason);
|
|
86
|
+
}
|
|
87
|
+
isBlockingWorkflowGate(task) {
|
|
88
|
+
if (task.taskId === "wait:input")
|
|
89
|
+
return true;
|
|
90
|
+
if (task.taskId === "wait:children" || task.taskId === "wait:blockers" || task.taskId.startsWith("wait:active-run:")) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (task.taskId === "wait:authority") {
|
|
94
|
+
return this.workflowAuthorityObserved(task.projectId, task.subjectId);
|
|
95
|
+
}
|
|
96
|
+
return task.taskType === "verify" || task.taskType === "ask" || task.taskType === "escalate" || task.taskType === "publish";
|
|
97
|
+
}
|
|
98
|
+
resolveDispatchableWake(projectId, linearIssueId, issue, options) {
|
|
99
|
+
const existingWorkflowTasks = this.listOpenWorkflowTasks(projectId, linearIssueId);
|
|
100
|
+
const existingWorkflowTask = this.peekRunnableWorkflowTask(projectId, linearIssueId, existingWorkflowTasks);
|
|
101
|
+
if (existingWorkflowTask?.runType) {
|
|
43
102
|
return {
|
|
44
|
-
runType:
|
|
45
|
-
|
|
46
|
-
eventIds:
|
|
47
|
-
source: "
|
|
103
|
+
runType: existingWorkflowTask.runType,
|
|
104
|
+
wakeReason: existingWorkflowTask.taskId,
|
|
105
|
+
eventIds: [],
|
|
106
|
+
source: "workflow_task",
|
|
48
107
|
};
|
|
49
108
|
}
|
|
50
|
-
const
|
|
109
|
+
const freshIssue = this.db.issues.getIssue(projectId, linearIssueId) ?? issue;
|
|
110
|
+
const openWorkflowTasks = this.reconcileOpenWorkflowTasks(freshIssue, options);
|
|
111
|
+
const workflowTask = this.peekRunnableWorkflowTask(projectId, linearIssueId, openWorkflowTasks);
|
|
51
112
|
if (workflowTask?.runType) {
|
|
52
113
|
return {
|
|
53
114
|
runType: workflowTask.runType,
|
|
@@ -56,6 +117,21 @@ export class WakeDispatcher {
|
|
|
56
117
|
source: "workflow_task",
|
|
57
118
|
};
|
|
58
119
|
}
|
|
120
|
+
const sessionWake = this.db.issueSessions.peekIssueSessionWake(projectId, linearIssueId);
|
|
121
|
+
if (sessionWake) {
|
|
122
|
+
if (this.workflowTasksSuppressSessionWake(openWorkflowTasks, sessionWake.wakeReason)) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
runType: sessionWake.runType,
|
|
127
|
+
...(sessionWake.wakeReason ? { wakeReason: sessionWake.wakeReason } : {}),
|
|
128
|
+
eventIds: sessionWake.eventIds,
|
|
129
|
+
source: "session_event",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (this.workflowTasksSuppressSessionWake(openWorkflowTasks, undefined)) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
59
135
|
if (issue.pendingRunType) {
|
|
60
136
|
return {
|
|
61
137
|
runType: issue.pendingRunType,
|
|
@@ -279,7 +355,17 @@ export class WakeDispatcher {
|
|
|
279
355
|
runType: params.run.runType,
|
|
280
356
|
});
|
|
281
357
|
const issue = this.db.issues.getIssue(params.run.projectId, params.run.linearIssueId);
|
|
282
|
-
|
|
358
|
+
if (issue?.factoryState === "done" || issue?.factoryState === "failed" || issue?.factoryState === "escalated" || issue?.prState === "merged") {
|
|
359
|
+
emitTelemetry(this.telemetry, {
|
|
360
|
+
type: "wake.suppressed",
|
|
361
|
+
projectId: params.run.projectId,
|
|
362
|
+
linearIssueId: params.run.linearIssueId,
|
|
363
|
+
...(params.issueKey ? { issueKey: params.issueKey } : {}),
|
|
364
|
+
reason: "terminal_event",
|
|
365
|
+
});
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
const wake = issue ? this.resolveDispatchableWake(params.run.projectId, params.run.linearIssueId, issue, { ignoreDetachedActiveRuns: true }) : undefined;
|
|
283
369
|
if (!wake) {
|
|
284
370
|
emitTelemetry(this.telemetry, {
|
|
285
371
|
type: "wake.suppressed",
|
package/dist/workflow-runtime.js
CHANGED
|
@@ -197,6 +197,9 @@ export function deriveWorkflowTasks(snapshot) {
|
|
|
197
197
|
if (snapshot.status === "done") {
|
|
198
198
|
return [];
|
|
199
199
|
}
|
|
200
|
+
if (snapshot.status === "failed") {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
200
203
|
if (snapshot.activeRun) {
|
|
201
204
|
return [{
|
|
202
205
|
id: `wait:active-run:${snapshot.activeRun.id}`,
|
|
@@ -2,13 +2,21 @@ import { evaluateTaskStart, projectWorkflowSnapshot, } from "./workflow-runtime.
|
|
|
2
2
|
function isActiveRun(run) {
|
|
3
3
|
return run.status === "queued" || run.status === "running";
|
|
4
4
|
}
|
|
5
|
-
function resolveActiveRunSnapshot(db, issue) {
|
|
5
|
+
function resolveActiveRunSnapshot(db, issue, options) {
|
|
6
6
|
const pinnedRun = issue.activeRunId !== undefined ? db.runs.getRunById(issue.activeRunId) : undefined;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.
|
|
11
|
-
.
|
|
7
|
+
if (pinnedRun && isActiveRun(pinnedRun)) {
|
|
8
|
+
return {
|
|
9
|
+
id: pinnedRun.id,
|
|
10
|
+
runType: pinnedRun.runType,
|
|
11
|
+
authorityEpoch: pinnedRun.authorityEpoch,
|
|
12
|
+
status: pinnedRun.status,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (options?.ignoreDetachedActiveRuns)
|
|
16
|
+
return undefined;
|
|
17
|
+
const run = db.runs.listRunsForIssue(issue.projectId, issue.linearIssueId)
|
|
18
|
+
.filter(isActiveRun)
|
|
19
|
+
.at(-1);
|
|
12
20
|
if (!run)
|
|
13
21
|
return undefined;
|
|
14
22
|
return {
|
|
@@ -34,8 +42,8 @@ function readinessForTask(snapshot, task) {
|
|
|
34
42
|
}
|
|
35
43
|
return evaluateTaskStart(snapshot, task);
|
|
36
44
|
}
|
|
37
|
-
export function buildWorkflowSnapshotForIssue(db, issue) {
|
|
38
|
-
const activeRun = resolveActiveRunSnapshot(db, issue);
|
|
45
|
+
export function buildWorkflowSnapshotForIssue(db, issue, options) {
|
|
46
|
+
const activeRun = resolveActiveRunSnapshot(db, issue, options);
|
|
39
47
|
return projectWorkflowSnapshot({
|
|
40
48
|
issue,
|
|
41
49
|
observations: db.workflowObservations.listObservations(issue.projectId, issue.linearIssueId),
|
|
@@ -45,8 +53,8 @@ export function buildWorkflowSnapshotForIssue(db, issue) {
|
|
|
45
53
|
...(activeRun ? { activeRun } : {}),
|
|
46
54
|
});
|
|
47
55
|
}
|
|
48
|
-
export function reconcileWorkflowTasksForIssue(db, issue) {
|
|
49
|
-
const snapshot = buildWorkflowSnapshotForIssue(db, issue);
|
|
56
|
+
export function reconcileWorkflowTasksForIssue(db, issue, options) {
|
|
57
|
+
const snapshot = buildWorkflowSnapshotForIssue(db, issue, options);
|
|
50
58
|
const result = db.workflowTasks.reconcileTasks({
|
|
51
59
|
projectId: issue.projectId,
|
|
52
60
|
subjectId: issue.linearIssueId,
|