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.
@@ -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 (shouldRecoverReactivePrWork && reactiveIntent) {
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
- const hasPendingWake = this.db.workflowWakes.peekIssueWake(project.id, liveIssue.id) !== undefined;
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 and queued implementation"
269
+ ? "Discovered delegated Linear issue during startup recovery"
277
270
  : "Discovered delegated blocked Linear issue during startup recovery");
278
271
  }
279
- appendReactiveWakeEvent(projectId, linearIssueId, issue, runType) {
280
- const eventType = reactiveWakeEventType(runType);
281
- const dedupeKey = runType === "queue_repair" || runType === "ci_repair"
282
- ? buildRepairWakeDedupeKey({
283
- scope: "startup_recovery",
284
- runType,
285
- linearIssueId,
286
- signature: issue.lastGitHubFailureSignature,
287
- prHeadSha: issue.prHeadSha,
288
- failureHeadSha: issue.lastGitHubFailureHeadSha,
289
- })
290
- : buildRequestedChangesWakeIdentity({
291
- linearIssueId,
292
- runType,
293
- headSha: issue.prHeadSha,
294
- }).dedupeKey;
295
- const requestedChangesIdentity = eventType === "review_changes_requested"
296
- ? buildRequestedChangesWakeIdentity({
297
- linearIssueId,
298
- runType: runType === "branch_upkeep" ? "branch_upkeep" : "review_fix",
299
- headSha: issue.prHeadSha,
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
@@ -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 wake = this.db.workflowWakes.peekIssueWake(projectId, linearIssueId);
148
- // Fall back to the legacy pending_run_type column. The orchestrator
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
- ...(wake?.wakeReason ? { wakeReason: wake.wakeReason } : {}),
180
- ...(wake?.eventIds ? { eventIds: wake.eventIds } : {}),
181
- source: wake ? (wake.eventIds.length > 0 ? "session_event" : "implicit") : "legacy_pending_run_type",
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
- ...(wake?.wakeReason ? { wakeReason: wake.wakeReason } : {}),
193
- ...(wake?.eventIds ? { eventIds: wake.eventIds } : {}),
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
- ...(wake?.wakeReason ? { wakeReason: wake.wakeReason } : {}),
213
- ...(wake?.eventIds ? { eventIds: wake.eventIds } : {}),
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 wake = this.db.workflowWakes.peekIssueWake(params.run.projectId, params.run.linearIssueId);
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",
@@ -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
- if (result.wakeRunType && !wakeAlreadyQueuedByFollowUpHandler) {
175
- const queuedRunType = this.enqueuePendingSessionWake(project.id, issue.id);
176
- this.feed?.publish({
177
- level: "info",
178
- kind: "stage",
179
- issueKey: issue.identifier,
180
- projectId: project.id,
181
- stage: queuedRunType ?? result.wakeRunType,
182
- status: "queued",
183
- summary: `Queued ${(queuedRunType ?? result.wakeRunType)} workflow`,
184
- detail: `Triggered by ${hydrated.triggerEvent}.`,
185
- });
186
- }
187
- for (const dependentIssueId of newlyReadyDependents) {
188
- // The dependency-readiness handler already dispatched via the
189
- // wake dispatcher; here we just emit the operator-feed event so
190
- // the dispatched run shows up in the timeline.
191
- const dependent = this.db.getTrackedIssue(project.id, dependentIssueId);
192
- const queuedRunType = this.peekPendingSessionWakeRunType(project.id, dependentIssueId);
193
- this.feed?.publish({
194
- level: "info",
195
- kind: "stage",
196
- issueKey: dependent?.issueKey,
197
- projectId: project.id,
198
- stage: queuedRunType ?? "implementation",
199
- status: "queued",
200
- summary: `Queued ${(queuedRunType ?? "implementation")} after blockers resolved`,
201
- detail: `All blockers are now done for ${dependent?.issueKey ?? dependentIssueId}.`,
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,