patchrelay 0.81.0 → 0.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli/cluster-health/index.js +10 -3
- package/dist/db/migrations.js +43 -0
- package/dist/db/run-store.js +62 -4
- package/dist/db/schema-guard.js +2 -0
- package/dist/db/workflow-observation-store.js +61 -0
- package/dist/db/workflow-task-store.js +111 -0
- package/dist/db.js +60 -3
- package/dist/github-review-context.js +90 -0
- package/dist/github-webhook-handler.js +64 -11
- package/dist/idle-reconciliation.js +33 -6
- package/dist/issue-overview-query.js +2 -1
- package/dist/linear-issue-projection.js +37 -0
- package/dist/run-context.js +6 -6
- package/dist/run-finalizer.js +102 -22
- package/dist/run-launcher.js +1 -0
- package/dist/run-orchestrator.js +7 -0
- package/dist/run-wake-planner.js +58 -8
- package/dist/service-startup-recovery.js +51 -61
- package/dist/tracked-issue-list-query.js +5 -1
- package/dist/wake-dispatcher.js +59 -21
- package/dist/webhook-handler.js +75 -30
- package/dist/webhooks/dependency-readiness-handler.js +19 -13
- package/dist/webhooks/desired-stage-recorder.js +3 -18
- package/dist/workflow-runtime.js +381 -0
- package/dist/workflow-task-reconciler.js +64 -0
- package/package.json +1 -1
- package/dist/github-webhook-reactive-run.js +0 -309
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { appendDelegationObservedEvent } from "./delegation-audit.js";
|
|
2
2
|
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
3
3
|
import { isResumablePausedLocalWork } from "./paused-issue-state.js";
|
|
4
|
-
import { buildRepairWakeDedupeKey, buildRequestedChangesWakeIdentity, reactiveWakeEventType } from "./reactive-wake-keys.js";
|
|
5
4
|
import { upsertLinearIssueProjection } from "./linear-issue-projection.js";
|
|
5
|
+
import { reconcileWorkflowTasksForIssue } from "./workflow-task-reconciler.js";
|
|
6
6
|
const WRITER = "service-startup-recovery";
|
|
7
7
|
export class ServiceStartupRecovery {
|
|
8
8
|
config;
|
|
@@ -81,6 +81,14 @@ export class ServiceStartupRecovery {
|
|
|
81
81
|
issue = this.db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
82
82
|
const delegated = liveIssue.delegateId === installation.actorId;
|
|
83
83
|
if (issue.delegatedToPatchRelay !== delegated) {
|
|
84
|
+
this.appendAuthorityObservation({
|
|
85
|
+
projectId: issue.projectId,
|
|
86
|
+
linearIssueId: issue.linearIssueId,
|
|
87
|
+
delegated,
|
|
88
|
+
actorId: installation.actorId,
|
|
89
|
+
observedDelegateId: liveIssue.delegateId,
|
|
90
|
+
reason: "startup_recovery_refreshed_linear_delegation",
|
|
91
|
+
});
|
|
84
92
|
appendDelegationObservedEvent(this.db, {
|
|
85
93
|
projectId: issue.projectId,
|
|
86
94
|
linearIssueId: issue.linearIssueId,
|
|
@@ -100,7 +108,8 @@ export class ServiceStartupRecovery {
|
|
|
100
108
|
}
|
|
101
109
|
const unresolvedBlockers = this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
|
|
102
110
|
const latestRun = this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
103
|
-
const hasPendingWake = this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId) !== undefined
|
|
111
|
+
const hasPendingWake = this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId) !== undefined
|
|
112
|
+
|| this.db.workflowTasks.listOpenRunnableTasks(issue.projectId).some((task) => task.subjectId === issue.linearIssueId);
|
|
104
113
|
const shouldRecoverPausedLocalWork = delegated
|
|
105
114
|
&& isResumablePausedLocalWork({
|
|
106
115
|
issue: {
|
|
@@ -158,18 +167,7 @@ export class ServiceStartupRecovery {
|
|
|
158
167
|
continue;
|
|
159
168
|
}
|
|
160
169
|
if (unresolvedBlockers === 0) {
|
|
161
|
-
if (
|
|
162
|
-
this.appendReactiveWakeEvent(issue.projectId, issue.linearIssueId, issue, reactiveIntent.runType);
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
166
|
-
projectId: issue.projectId,
|
|
167
|
-
linearIssueId: issue.linearIssueId,
|
|
168
|
-
eventType: "delegated",
|
|
169
|
-
dedupeKey: `delegated:${issue.linearIssueId}`,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
if (this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId)) {
|
|
170
|
+
if (this.reconcileAndFindRunnableTask(updated.projectId, updated.linearIssueId)) {
|
|
173
171
|
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
174
172
|
}
|
|
175
173
|
this.logger.info({
|
|
@@ -255,61 +253,53 @@ export class ServiceStartupRecovery {
|
|
|
255
253
|
if (commit.outcome !== "applied")
|
|
256
254
|
return;
|
|
257
255
|
const updated = commit.issue;
|
|
258
|
-
|
|
256
|
+
this.appendAuthorityObservation({
|
|
257
|
+
projectId: project.id,
|
|
258
|
+
linearIssueId: liveIssue.id,
|
|
259
|
+
delegated: true,
|
|
260
|
+
observedDelegateId: liveIssue.delegateId,
|
|
261
|
+
reason: "startup_recovery_discovered_delegated_issue",
|
|
262
|
+
});
|
|
259
263
|
const unresolvedBlockers = this.db.issues.countUnresolvedBlockers(project.id, liveIssue.id);
|
|
260
|
-
if (!hasPendingWake && unresolvedBlockers === 0) {
|
|
261
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(project.id, liveIssue.id, {
|
|
262
|
-
projectId: project.id,
|
|
263
|
-
linearIssueId: liveIssue.id,
|
|
264
|
-
eventType: "delegated",
|
|
265
|
-
dedupeKey: `delegated:${liveIssue.id}`,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
if (this.db.workflowWakes.peekIssueWake(project.id, liveIssue.id)) {
|
|
269
|
-
this.enqueueIssue(project.id, liveIssue.id);
|
|
270
|
-
}
|
|
271
264
|
this.logger.info({
|
|
272
265
|
issueKey: updated.issueKey,
|
|
273
266
|
projectId: project.id,
|
|
274
267
|
unresolvedBlockers,
|
|
275
268
|
}, unresolvedBlockers === 0
|
|
276
|
-
? "Discovered delegated Linear issue during startup recovery
|
|
269
|
+
? "Discovered delegated Linear issue during startup recovery"
|
|
277
270
|
: "Discovered delegated blocked Linear issue during startup recovery");
|
|
278
271
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
})
|
|
301
|
-
: undefined;
|
|
302
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(projectId, linearIssueId, {
|
|
303
|
-
projectId,
|
|
304
|
-
linearIssueId,
|
|
305
|
-
eventType,
|
|
306
|
-
...(requestedChangesIdentity ? {
|
|
307
|
-
eventJson: JSON.stringify({
|
|
308
|
-
requestedChangesCoalesceKey: requestedChangesIdentity.coalesceKey,
|
|
309
|
-
...(requestedChangesIdentity.headSha ? { requestedChangesHeadSha: requestedChangesIdentity.headSha } : {}),
|
|
310
|
-
}),
|
|
311
|
-
} : {}),
|
|
312
|
-
dedupeKey,
|
|
272
|
+
appendAuthorityObservation(params) {
|
|
273
|
+
this.db.workflowObservations.appendObservation({
|
|
274
|
+
projectId: params.projectId,
|
|
275
|
+
subjectId: params.linearIssueId,
|
|
276
|
+
source: "linear",
|
|
277
|
+
type: params.delegated ? "linear.delegated" : "linear.undelegated",
|
|
278
|
+
payloadJson: JSON.stringify({
|
|
279
|
+
source: "startup_recovery",
|
|
280
|
+
delegated: params.delegated,
|
|
281
|
+
issueId: params.linearIssueId,
|
|
282
|
+
actorId: params.actorId,
|
|
283
|
+
observedDelegateId: params.observedDelegateId,
|
|
284
|
+
reason: params.reason,
|
|
285
|
+
}),
|
|
286
|
+
dedupeKey: [
|
|
287
|
+
"startup_recovery",
|
|
288
|
+
"authority",
|
|
289
|
+
params.linearIssueId,
|
|
290
|
+
params.delegated ? "delegated" : "undelegated",
|
|
291
|
+
params.observedDelegateId ?? "",
|
|
292
|
+
].join(":"),
|
|
313
293
|
});
|
|
314
294
|
}
|
|
295
|
+
reconcileAndFindRunnableTask(projectId, linearIssueId) {
|
|
296
|
+
const issue = this.db.issues.getIssue(projectId, linearIssueId);
|
|
297
|
+
if (!issue)
|
|
298
|
+
return false;
|
|
299
|
+
const reconciliation = reconcileWorkflowTasksForIssue(this.db, issue);
|
|
300
|
+
return [
|
|
301
|
+
...reconciliation.result.opened,
|
|
302
|
+
...reconciliation.result.updated,
|
|
303
|
+
].some((task) => task.gateAction === "start" && task.runType);
|
|
304
|
+
}
|
|
315
305
|
}
|
|
@@ -85,8 +85,12 @@ export class TrackedIssueListQuery {
|
|
|
85
85
|
const blockedByKeys = parseStringArray(typeof row.blocked_by_keys_json === "string" ? row.blocked_by_keys_json : undefined);
|
|
86
86
|
const blockedByCount = Number(row.blocked_by_count ?? 0);
|
|
87
87
|
const hasPendingSessionEvents = Number(row.pending_session_event_count ?? 0) > 0;
|
|
88
|
+
const hasRunnableWorkflowTask = this.db.workflowTasks
|
|
89
|
+
.listOpenRunnableTasks(String(row.project_id))
|
|
90
|
+
.some((task) => task.subjectId === String(row.linear_issue_id));
|
|
88
91
|
const hasPendingWake = hasPendingSessionEvents
|
|
89
|
-
|| this.db.workflowWakes.peekIssueWake(String(row.project_id), String(row.linear_issue_id)) !== undefined
|
|
92
|
+
|| this.db.workflowWakes.peekIssueWake(String(row.project_id), String(row.linear_issue_id)) !== undefined
|
|
93
|
+
|| hasRunnableWorkflowTask;
|
|
90
94
|
const detachedActiveRun = hasDetachedActiveLatestRun({
|
|
91
95
|
activeRunId: row.active_run_type !== null ? 1 : undefined,
|
|
92
96
|
latestRun: row.latest_run_status !== null
|
package/dist/wake-dispatcher.js
CHANGED
|
@@ -32,6 +32,47 @@ export class WakeDispatcher {
|
|
|
32
32
|
this.feed = feed;
|
|
33
33
|
this.telemetry = telemetry;
|
|
34
34
|
}
|
|
35
|
+
peekRunnableWorkflowTask(projectId, linearIssueId) {
|
|
36
|
+
return this.db.workflowTasks
|
|
37
|
+
.listOpenRunnableTasks(projectId)
|
|
38
|
+
.find((task) => task.subjectId === linearIssueId && task.runType !== undefined);
|
|
39
|
+
}
|
|
40
|
+
resolveDispatchableWake(projectId, linearIssueId, issue) {
|
|
41
|
+
const sessionWake = this.db.issueSessions.peekIssueSessionWake(projectId, linearIssueId);
|
|
42
|
+
if (sessionWake) {
|
|
43
|
+
return {
|
|
44
|
+
runType: sessionWake.runType,
|
|
45
|
+
...(sessionWake.wakeReason ? { wakeReason: sessionWake.wakeReason } : {}),
|
|
46
|
+
eventIds: sessionWake.eventIds,
|
|
47
|
+
source: "session_event",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const workflowTask = this.peekRunnableWorkflowTask(projectId, linearIssueId);
|
|
51
|
+
if (workflowTask?.runType) {
|
|
52
|
+
return {
|
|
53
|
+
runType: workflowTask.runType,
|
|
54
|
+
wakeReason: workflowTask.taskId,
|
|
55
|
+
eventIds: [],
|
|
56
|
+
source: "workflow_task",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (issue.pendingRunType) {
|
|
60
|
+
return {
|
|
61
|
+
runType: issue.pendingRunType,
|
|
62
|
+
eventIds: [],
|
|
63
|
+
source: "legacy_pending_run_type",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const implicitWake = this.db.workflowWakes.peekIssueWake(projectId, linearIssueId);
|
|
67
|
+
if (!implicitWake)
|
|
68
|
+
return undefined;
|
|
69
|
+
return {
|
|
70
|
+
runType: implicitWake.runType,
|
|
71
|
+
...(implicitWake.wakeReason ? { wakeReason: implicitWake.wakeReason } : {}),
|
|
72
|
+
eventIds: implicitWake.eventIds,
|
|
73
|
+
source: "implicit",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
35
76
|
// Scope the next enqueue calls inside `fn` to a single dedupe Set.
|
|
36
77
|
// Nested ticks reuse the outermost Set so deeply nested helpers do
|
|
37
78
|
// not silently lose dedupe.
|
|
@@ -144,12 +185,8 @@ export class WakeDispatcher {
|
|
|
144
185
|
}
|
|
145
186
|
return undefined;
|
|
146
187
|
}
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
// materializes it into a real event at run time, but the poke still
|
|
150
|
-
// needs to happen now so the orchestrator gets called at all.
|
|
151
|
-
const runType = wake?.runType ?? issue.pendingRunType;
|
|
152
|
-
if (!runType) {
|
|
188
|
+
const dispatchable = this.resolveDispatchableWake(projectId, linearIssueId, issue);
|
|
189
|
+
if (!dispatchable) {
|
|
153
190
|
emitTelemetry(this.telemetry, {
|
|
154
191
|
type: "wake.suppressed",
|
|
155
192
|
projectId,
|
|
@@ -175,10 +212,10 @@ export class WakeDispatcher {
|
|
|
175
212
|
projectId,
|
|
176
213
|
linearIssueId,
|
|
177
214
|
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
178
|
-
runType,
|
|
179
|
-
...(
|
|
180
|
-
|
|
181
|
-
source:
|
|
215
|
+
runType: dispatchable.runType,
|
|
216
|
+
...(dispatchable.wakeReason ? { wakeReason: dispatchable.wakeReason } : {}),
|
|
217
|
+
eventIds: dispatchable.eventIds,
|
|
218
|
+
source: dispatchable.source,
|
|
182
219
|
});
|
|
183
220
|
const tick = options?.enqueuedThisTick ?? this.currentTick;
|
|
184
221
|
const key = `${projectId}:${linearIssueId}`;
|
|
@@ -188,18 +225,18 @@ export class WakeDispatcher {
|
|
|
188
225
|
projectId,
|
|
189
226
|
linearIssueId,
|
|
190
227
|
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
191
|
-
runType,
|
|
192
|
-
...(
|
|
193
|
-
|
|
228
|
+
runType: dispatchable.runType,
|
|
229
|
+
...(dispatchable.wakeReason ? { wakeReason: dispatchable.wakeReason } : {}),
|
|
230
|
+
eventIds: dispatchable.eventIds,
|
|
194
231
|
});
|
|
195
232
|
emitTelemetry(this.telemetry, {
|
|
196
233
|
type: "queue.deduped",
|
|
197
234
|
projectId,
|
|
198
235
|
linearIssueId,
|
|
199
236
|
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
200
|
-
runType,
|
|
237
|
+
runType: dispatchable.runType,
|
|
201
238
|
});
|
|
202
|
-
return runType;
|
|
239
|
+
return dispatchable.runType;
|
|
203
240
|
}
|
|
204
241
|
tick?.add(key);
|
|
205
242
|
this.enqueueIssue(projectId, linearIssueId);
|
|
@@ -208,18 +245,18 @@ export class WakeDispatcher {
|
|
|
208
245
|
projectId,
|
|
209
246
|
linearIssueId,
|
|
210
247
|
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
211
|
-
runType,
|
|
212
|
-
...(
|
|
213
|
-
|
|
248
|
+
runType: dispatchable.runType,
|
|
249
|
+
...(dispatchable.wakeReason ? { wakeReason: dispatchable.wakeReason } : {}),
|
|
250
|
+
eventIds: dispatchable.eventIds,
|
|
214
251
|
});
|
|
215
252
|
emitTelemetry(this.telemetry, {
|
|
216
253
|
type: "queue.enqueued",
|
|
217
254
|
projectId,
|
|
218
255
|
linearIssueId,
|
|
219
256
|
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
220
|
-
runType,
|
|
257
|
+
runType: dispatchable.runType,
|
|
221
258
|
});
|
|
222
|
-
return runType;
|
|
259
|
+
return dispatchable.runType;
|
|
223
260
|
}
|
|
224
261
|
// Release the lease for a finished run, then drain any wake that
|
|
225
262
|
// landed during the run. The single owner of "run is over, what's
|
|
@@ -241,7 +278,8 @@ export class WakeDispatcher {
|
|
|
241
278
|
runId: params.run.id,
|
|
242
279
|
runType: params.run.runType,
|
|
243
280
|
});
|
|
244
|
-
const
|
|
281
|
+
const issue = this.db.issues.getIssue(params.run.projectId, params.run.linearIssueId);
|
|
282
|
+
const wake = issue ? this.resolveDispatchableWake(params.run.projectId, params.run.linearIssueId, issue) : undefined;
|
|
245
283
|
if (!wake) {
|
|
246
284
|
emitTelemetry(this.telemetry, {
|
|
247
285
|
type: "wake.suppressed",
|
package/dist/webhook-handler.js
CHANGED
|
@@ -15,6 +15,7 @@ import { WakeDispatcher } from "./wake-dispatcher.js";
|
|
|
15
15
|
import { CodexFollowupIntentClassifier } from "./followup-intent.js";
|
|
16
16
|
import { AgentInputService } from "./agent-input-service.js";
|
|
17
17
|
import { noopTelemetry } from "./telemetry.js";
|
|
18
|
+
import { reconcileWorkflowTasksForIssue } from "./workflow-task-reconciler.js";
|
|
18
19
|
export class WebhookHandler {
|
|
19
20
|
config;
|
|
20
21
|
db;
|
|
@@ -139,6 +140,33 @@ export class WebhookHandler {
|
|
|
139
140
|
stopActiveRun: (run, input) => this.stopActiveRun(run, input),
|
|
140
141
|
});
|
|
141
142
|
const trackedIssue = result.issue;
|
|
143
|
+
this.db.workflowObservations.appendObservation({
|
|
144
|
+
projectId: project.id,
|
|
145
|
+
subjectId: issue.id,
|
|
146
|
+
source: "linear",
|
|
147
|
+
type: hydrated.triggerEvent === "delegateChanged"
|
|
148
|
+
? result.delegated ? "linear.delegated" : "linear.undelegated"
|
|
149
|
+
: `linear.${hydrated.triggerEvent}`,
|
|
150
|
+
payloadJson: JSON.stringify({
|
|
151
|
+
triggerEvent: hydrated.triggerEvent,
|
|
152
|
+
webhookId: hydrated.webhookId,
|
|
153
|
+
delegated: result.delegated,
|
|
154
|
+
issueId: issue.id,
|
|
155
|
+
issueKey: issue.identifier,
|
|
156
|
+
agentSessionId: hydrated.agentSession?.id,
|
|
157
|
+
promptContext: hydrated.agentSession?.promptContext?.trim(),
|
|
158
|
+
promptBody: hydrated.agentSession?.promptBody?.trim(),
|
|
159
|
+
actorId: hydrated.actor?.id,
|
|
160
|
+
actorName: hydrated.actor?.name,
|
|
161
|
+
}),
|
|
162
|
+
dedupeKey: hydrated.webhookId,
|
|
163
|
+
});
|
|
164
|
+
const observedIssue = this.db.getIssue(project.id, issue.id);
|
|
165
|
+
let openedRunnableWorkflowTask = false;
|
|
166
|
+
if (observedIssue) {
|
|
167
|
+
const workflowReconciliation = reconcileWorkflowTasksForIssue(this.db, observedIssue);
|
|
168
|
+
openedRunnableWorkflowTask = workflowReconciliation.result.opened.some((task) => task.gateAction === "start" && task.runType);
|
|
169
|
+
}
|
|
142
170
|
const newlyReadyDependents = this.dependencyReadinessHandler.reconcile(project.id, issue.id);
|
|
143
171
|
const syncTargets = new Set(shouldSyncLinearStateAfterWebhook(hydrated.triggerEvent)
|
|
144
172
|
? [issue.id, ...newlyReadyDependents]
|
|
@@ -171,36 +199,53 @@ export class WebhookHandler {
|
|
|
171
199
|
const wakeAlreadyQueuedByFollowUpHandler = normalized.triggerEvent === "commentCreated"
|
|
172
200
|
|| normalized.triggerEvent === "commentUpdated"
|
|
173
201
|
|| normalized.triggerEvent === "agentPrompted";
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
202
|
+
await this.wakeDispatcher.withTick(async () => {
|
|
203
|
+
if (result.wakeRunType && !wakeAlreadyQueuedByFollowUpHandler) {
|
|
204
|
+
const queuedRunType = this.enqueuePendingSessionWake(project.id, issue.id);
|
|
205
|
+
this.feed?.publish({
|
|
206
|
+
level: "info",
|
|
207
|
+
kind: "stage",
|
|
208
|
+
issueKey: issue.identifier,
|
|
209
|
+
projectId: project.id,
|
|
210
|
+
stage: queuedRunType ?? result.wakeRunType,
|
|
211
|
+
status: "queued",
|
|
212
|
+
summary: `Queued ${(queuedRunType ?? result.wakeRunType)} workflow`,
|
|
213
|
+
detail: `Triggered by ${hydrated.triggerEvent}.`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (openedRunnableWorkflowTask && !wakeAlreadyQueuedByFollowUpHandler) {
|
|
217
|
+
const workflowTaskRunType = this.enqueuePendingSessionWake(project.id, issue.id);
|
|
218
|
+
if (workflowTaskRunType && !result.wakeRunType) {
|
|
219
|
+
this.feed?.publish({
|
|
220
|
+
level: "info",
|
|
221
|
+
kind: "stage",
|
|
222
|
+
issueKey: issue.identifier,
|
|
223
|
+
projectId: project.id,
|
|
224
|
+
stage: workflowTaskRunType,
|
|
225
|
+
status: "queued",
|
|
226
|
+
summary: `Queued ${workflowTaskRunType} workflow`,
|
|
227
|
+
detail: `Derived after ${hydrated.triggerEvent}.`,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
for (const dependentIssueId of newlyReadyDependents) {
|
|
232
|
+
// The dependency-readiness handler already dispatched via the
|
|
233
|
+
// wake dispatcher; here we just emit the operator-feed event so
|
|
234
|
+
// the dispatched run shows up in the timeline.
|
|
235
|
+
const dependent = this.db.getTrackedIssue(project.id, dependentIssueId);
|
|
236
|
+
const queuedRunType = this.peekPendingSessionWakeRunType(project.id, dependentIssueId);
|
|
237
|
+
this.feed?.publish({
|
|
238
|
+
level: "info",
|
|
239
|
+
kind: "stage",
|
|
240
|
+
issueKey: dependent?.issueKey,
|
|
241
|
+
projectId: project.id,
|
|
242
|
+
stage: queuedRunType ?? "implementation",
|
|
243
|
+
status: "queued",
|
|
244
|
+
summary: `Queued ${(queuedRunType ?? "implementation")} after blockers resolved`,
|
|
245
|
+
detail: `All blockers are now done for ${dependent?.issueKey ?? dependentIssueId}.`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
});
|
|
204
249
|
for (const issueId of syncTargets) {
|
|
205
250
|
const syncIssue = this.db.getIssue(project.id, issueId);
|
|
206
251
|
if (!syncIssue) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { emitTelemetry, noopTelemetry } from "../telemetry.js";
|
|
2
|
+
import { reconcileWorkflowTasksForIssue } from "../workflow-task-reconciler.js";
|
|
2
3
|
const WRITER = "dependency-readiness-handler";
|
|
3
4
|
export class DependencyReadinessHandler {
|
|
4
5
|
db;
|
|
@@ -57,6 +58,11 @@ export class DependencyReadinessHandler {
|
|
|
57
58
|
if (!issue.delegatedToPatchRelay || issue.activeRunId !== undefined) {
|
|
58
59
|
continue;
|
|
59
60
|
}
|
|
61
|
+
const workflowReconciliation = reconcileWorkflowTasksForIssue(this.db, issue);
|
|
62
|
+
const hasRunnableWorkflowTask = [
|
|
63
|
+
...workflowReconciliation.result.opened,
|
|
64
|
+
...workflowReconciliation.result.updated,
|
|
65
|
+
].some((task) => task.gateAction === "start" && task.runType);
|
|
60
66
|
const pendingWakeRunType = this.db.workflowWakes.peekIssueWake(projectId, dependent.linearIssueId)?.runType
|
|
61
67
|
?? issue.pendingRunType;
|
|
62
68
|
if (pendingWakeRunType) {
|
|
@@ -72,6 +78,19 @@ export class DependencyReadinessHandler {
|
|
|
72
78
|
newlyReady.push(dependent.linearIssueId);
|
|
73
79
|
continue;
|
|
74
80
|
}
|
|
81
|
+
if (hasRunnableWorkflowTask) {
|
|
82
|
+
const dispatchedRunType = this.wakeDispatcher.dispatchIfWakePending(projectId, dependent.linearIssueId);
|
|
83
|
+
emitTelemetry(this.telemetry, {
|
|
84
|
+
type: "dependency.dependent_unblocked",
|
|
85
|
+
projectId,
|
|
86
|
+
linearIssueId: dependent.linearIssueId,
|
|
87
|
+
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
88
|
+
blockerLinearIssueId,
|
|
89
|
+
...(dispatchedRunType ? { dispatchedRunType } : {}),
|
|
90
|
+
});
|
|
91
|
+
newlyReady.push(dependent.linearIssueId);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
75
94
|
if (issue.factoryState !== "delegated" || this.db.issueSessions.hasPendingIssueSessionEvents(projectId, dependent.linearIssueId)) {
|
|
76
95
|
continue;
|
|
77
96
|
}
|
|
@@ -86,19 +105,6 @@ export class DependencyReadinessHandler {
|
|
|
86
105
|
},
|
|
87
106
|
});
|
|
88
107
|
}
|
|
89
|
-
const dispatchedRunType = this.wakeDispatcher.recordEventAndDispatch(projectId, dependent.linearIssueId, {
|
|
90
|
-
eventType: "delegated",
|
|
91
|
-
dedupeKey: `delegated:${dependent.linearIssueId}`,
|
|
92
|
-
});
|
|
93
|
-
emitTelemetry(this.telemetry, {
|
|
94
|
-
type: "dependency.dependent_unblocked",
|
|
95
|
-
projectId,
|
|
96
|
-
linearIssueId: dependent.linearIssueId,
|
|
97
|
-
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
98
|
-
blockerLinearIssueId,
|
|
99
|
-
...(dispatchedRunType ? { dispatchedRunType } : {}),
|
|
100
|
-
});
|
|
101
|
-
newlyReady.push(dependent.linearIssueId);
|
|
102
108
|
}
|
|
103
109
|
return newlyReady;
|
|
104
110
|
}
|
|
@@ -141,6 +141,9 @@ export class DesiredStageRecorder {
|
|
|
141
141
|
const wasResolved = isResolvedLinearState(existingIssue?.currentLinearStateType, existingIssue?.currentLinearState);
|
|
142
142
|
const isResolved = isResolvedLinearState(issue.currentLinearStateType, issue.currentLinearState);
|
|
143
143
|
if (workflowPlan.undelegation.factoryState) {
|
|
144
|
+
if (activeRun && releaseReason) {
|
|
145
|
+
this.db.runs.revokeRunLease(activeRun.id, { reason: releaseReason });
|
|
146
|
+
}
|
|
144
147
|
if (activeRun?.threadId && activeRun.turnId) {
|
|
145
148
|
await params.stopActiveRun(activeRun, "STOP: The issue was un-delegated from PatchRelay. Stop working immediately and exit.");
|
|
146
149
|
}
|
|
@@ -205,24 +208,6 @@ export class DesiredStageRecorder {
|
|
|
205
208
|
summary: "Waiting briefly for child issues to settle before orchestration starts",
|
|
206
209
|
});
|
|
207
210
|
}
|
|
208
|
-
else if (!workflowPlan.startupResume.factoryState
|
|
209
|
-
&& !workflowPlan.startupResume.pendingRunType
|
|
210
|
-
&& workflowPlan.desiredStage === "implementation"
|
|
211
|
-
&& params.normalized.triggerEvent !== "commentCreated"
|
|
212
|
-
&& params.normalized.triggerEvent !== "commentUpdated"
|
|
213
|
-
&& params.normalized.triggerEvent !== "agentPrompted") {
|
|
214
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.project.id, normalizedIssue.id, {
|
|
215
|
-
projectId: params.project.id,
|
|
216
|
-
linearIssueId: normalizedIssue.id,
|
|
217
|
-
eventType: "delegated",
|
|
218
|
-
eventJson: JSON.stringify({
|
|
219
|
-
promptContext: params.normalized.agentSession?.promptContext?.trim()
|
|
220
|
-
?? (issue.issueKey ? `Linear issue ${issue.issueKey} was delegated to PatchRelay.` : undefined),
|
|
221
|
-
promptBody: params.normalized.agentSession?.promptBody?.trim(),
|
|
222
|
-
}),
|
|
223
|
-
dedupeKey: `delegated:${normalizedIssue.id}`,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
211
|
if (previousParentIssueId && previousParentIssueId !== currentParentIssueId) {
|
|
227
212
|
wakeOrchestrationParentsForChildEvent({
|
|
228
213
|
db: this.db,
|