patchrelay 0.74.3 → 0.74.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/cli/data.js +1 -36
- package/dist/db/issue-session-store.js +78 -0
- package/dist/db/issue-store.js +38 -0
- package/dist/db/linear-installation-store.js +4 -0
- package/dist/failure-provenance.js +23 -0
- package/dist/github-webhook-state-projector.js +2 -11
- package/dist/idle-reconciliation.js +13 -24
- package/dist/interrupted-run-recovery.js +1 -3
- package/dist/no-pr-completion-check.js +2 -11
- package/dist/queue-health-monitor.js +7 -1
- package/dist/reactive-pr-state.js +9 -0
- package/dist/reactive-run-policy.js +2 -11
- package/dist/reactive-wake-keys.js +29 -0
- package/dist/run-finalizer.js +2 -13
- package/dist/run-launcher.js +2 -1
- package/dist/run-notification-handler.js +2 -4
- package/dist/run-orchestrator.js +1 -4
- package/dist/service-startup-recovery.js +16 -15
- package/dist/service.js +1 -2
- package/dist/tracked-issue-list-query.js +1 -70
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli/data.js
CHANGED
|
@@ -402,42 +402,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
402
402
|
});
|
|
403
403
|
}
|
|
404
404
|
list(options) {
|
|
405
|
-
const
|
|
406
|
-
const values = [];
|
|
407
|
-
if (options?.project) {
|
|
408
|
-
conditions.push("i.project_id = ?");
|
|
409
|
-
values.push(options.project);
|
|
410
|
-
}
|
|
411
|
-
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
412
|
-
const rows = this.db.connection
|
|
413
|
-
.prepare(`
|
|
414
|
-
SELECT
|
|
415
|
-
i.project_id,
|
|
416
|
-
i.linear_issue_id,
|
|
417
|
-
i.issue_key,
|
|
418
|
-
i.title,
|
|
419
|
-
i.current_linear_state,
|
|
420
|
-
i.factory_state,
|
|
421
|
-
i.updated_at,
|
|
422
|
-
s.session_state,
|
|
423
|
-
s.waiting_reason,
|
|
424
|
-
active_run.run_type AS active_run_type,
|
|
425
|
-
latest_run.run_type AS latest_run_type,
|
|
426
|
-
latest_run.status AS latest_run_status
|
|
427
|
-
FROM issues i
|
|
428
|
-
LEFT JOIN issue_sessions s
|
|
429
|
-
ON s.project_id = i.project_id
|
|
430
|
-
AND s.linear_issue_id = i.linear_issue_id
|
|
431
|
-
LEFT JOIN runs active_run ON active_run.id = i.active_run_id
|
|
432
|
-
LEFT JOIN runs latest_run ON latest_run.id = (
|
|
433
|
-
SELECT r.id FROM runs r
|
|
434
|
-
WHERE r.project_id = i.project_id AND r.linear_issue_id = i.linear_issue_id
|
|
435
|
-
ORDER BY r.id DESC LIMIT 1
|
|
436
|
-
)
|
|
437
|
-
${whereClause}
|
|
438
|
-
ORDER BY i.updated_at DESC, i.issue_key ASC
|
|
439
|
-
`)
|
|
440
|
-
.all(...values);
|
|
405
|
+
const rows = this.db.issues.listIssueSummaryRows(options?.project);
|
|
441
406
|
const items = rows.map((row) => {
|
|
442
407
|
const detachedActiveRun = row.active_run_type === null
|
|
443
408
|
&& (row.latest_run_status === "queued" || row.latest_run_status === "running");
|
|
@@ -292,4 +292,82 @@ export class IssueSessionStore {
|
|
|
292
292
|
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
293
293
|
this.releaseIssueSessionLease(projectId, linearIssueId, lease?.leaseId);
|
|
294
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Raw rows for the tracked-issue read model: one row per issue session joined
|
|
297
|
+
* to its issue, active/latest run, pending session-event count, and blocker
|
|
298
|
+
* rollup. Row shaping into the read model lives in the query layer; this owns
|
|
299
|
+
* only the SQL so schema knowledge stays in the persistence layer.
|
|
300
|
+
*/
|
|
301
|
+
listTrackedIssueRows() {
|
|
302
|
+
return this.connection
|
|
303
|
+
.prepare(`SELECT
|
|
304
|
+
s.project_id, s.linear_issue_id, s.issue_key, i.title,
|
|
305
|
+
i.current_linear_state, i.factory_state, i.delegated_to_patchrelay, s.session_state, s.waiting_reason, s.summary_text, s.display_updated_at,
|
|
306
|
+
i.pending_run_type,
|
|
307
|
+
i.orchestration_settle_until,
|
|
308
|
+
i.pr_number, i.pr_state, i.pr_head_sha, i.pr_review_state, i.pr_check_status, i.last_blocking_review_head_sha,
|
|
309
|
+
i.last_github_ci_snapshot_json,
|
|
310
|
+
i.last_github_failure_source,
|
|
311
|
+
i.last_github_failure_head_sha,
|
|
312
|
+
i.last_github_failure_check_name,
|
|
313
|
+
i.last_github_failure_context_json,
|
|
314
|
+
active_run.run_type AS active_run_type,
|
|
315
|
+
active_run.completion_check_thread_id AS active_completion_check_thread_id,
|
|
316
|
+
active_run.completion_check_outcome AS active_completion_check_outcome,
|
|
317
|
+
latest_run.run_type AS latest_run_type,
|
|
318
|
+
latest_run.status AS latest_run_status,
|
|
319
|
+
latest_run.summary_json AS latest_run_summary_json,
|
|
320
|
+
latest_run.report_json AS latest_run_report_json,
|
|
321
|
+
latest_run.completion_check_thread_id AS latest_run_completion_check_thread_id,
|
|
322
|
+
latest_run.completion_check_outcome AS latest_run_completion_check_outcome,
|
|
323
|
+
latest_run.completion_check_summary AS latest_run_completion_check_summary,
|
|
324
|
+
latest_run.completion_check_question AS latest_run_completion_check_question,
|
|
325
|
+
latest_run.completion_check_why AS latest_run_completion_check_why,
|
|
326
|
+
latest_run.completion_check_recommended_reply AS latest_run_completion_check_recommended_reply,
|
|
327
|
+
(
|
|
328
|
+
SELECT COUNT(*)
|
|
329
|
+
FROM issue_session_events e
|
|
330
|
+
WHERE e.project_id = s.project_id
|
|
331
|
+
AND e.linear_issue_id = s.linear_issue_id
|
|
332
|
+
AND e.processed_at IS NULL
|
|
333
|
+
) AS pending_session_event_count,
|
|
334
|
+
(
|
|
335
|
+
SELECT COUNT(*)
|
|
336
|
+
FROM issue_dependencies d
|
|
337
|
+
LEFT JOIN issues blockers
|
|
338
|
+
ON blockers.project_id = d.project_id
|
|
339
|
+
AND blockers.linear_issue_id = d.blocker_linear_issue_id
|
|
340
|
+
WHERE d.project_id = s.project_id
|
|
341
|
+
AND d.linear_issue_id = s.linear_issue_id
|
|
342
|
+
AND (
|
|
343
|
+
COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
|
|
344
|
+
AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
|
|
345
|
+
)
|
|
346
|
+
) AS blocked_by_count,
|
|
347
|
+
(
|
|
348
|
+
SELECT json_group_array(COALESCE(blockers.issue_key, d.blocker_issue_key, d.blocker_linear_issue_id))
|
|
349
|
+
FROM issue_dependencies d
|
|
350
|
+
LEFT JOIN issues blockers
|
|
351
|
+
ON blockers.project_id = d.project_id
|
|
352
|
+
AND blockers.linear_issue_id = d.blocker_linear_issue_id
|
|
353
|
+
WHERE d.project_id = s.project_id
|
|
354
|
+
AND d.linear_issue_id = s.linear_issue_id
|
|
355
|
+
AND (
|
|
356
|
+
COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
|
|
357
|
+
AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
|
|
358
|
+
)
|
|
359
|
+
) AS blocked_by_keys_json
|
|
360
|
+
FROM issue_sessions s
|
|
361
|
+
LEFT JOIN issues i
|
|
362
|
+
ON i.project_id = s.project_id
|
|
363
|
+
AND i.linear_issue_id = s.linear_issue_id
|
|
364
|
+
LEFT JOIN runs active_run ON active_run.id = COALESCE(s.active_run_id, i.active_run_id)
|
|
365
|
+
LEFT JOIN runs latest_run ON latest_run.id = (
|
|
366
|
+
SELECT r.id FROM runs r
|
|
367
|
+
WHERE r.project_id = s.project_id AND r.linear_issue_id = s.linear_issue_id
|
|
368
|
+
ORDER BY r.id DESC LIMIT 1
|
|
369
|
+
)
|
|
370
|
+
ORDER BY s.display_updated_at DESC, s.issue_key ASC`)
|
|
371
|
+
.all();
|
|
372
|
+
}
|
|
295
373
|
}
|
package/dist/db/issue-store.js
CHANGED
|
@@ -354,6 +354,44 @@ export class IssueStore {
|
|
|
354
354
|
return undefined;
|
|
355
355
|
}
|
|
356
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Raw rows for the CLI issue-summary read model (one row per issue joined to
|
|
359
|
+
* its session and active/latest run), optionally scoped to a project. Row
|
|
360
|
+
* shaping lives in the CLI layer; this owns only the SQL.
|
|
361
|
+
*/
|
|
362
|
+
listIssueSummaryRows(project) {
|
|
363
|
+
const whereClause = project ? "WHERE i.project_id = ?" : "";
|
|
364
|
+
const values = project ? [project] : [];
|
|
365
|
+
return this.connection
|
|
366
|
+
.prepare(`
|
|
367
|
+
SELECT
|
|
368
|
+
i.project_id,
|
|
369
|
+
i.linear_issue_id,
|
|
370
|
+
i.issue_key,
|
|
371
|
+
i.title,
|
|
372
|
+
i.current_linear_state,
|
|
373
|
+
i.factory_state,
|
|
374
|
+
i.updated_at,
|
|
375
|
+
s.session_state,
|
|
376
|
+
s.waiting_reason,
|
|
377
|
+
active_run.run_type AS active_run_type,
|
|
378
|
+
latest_run.run_type AS latest_run_type,
|
|
379
|
+
latest_run.status AS latest_run_status
|
|
380
|
+
FROM issues i
|
|
381
|
+
LEFT JOIN issue_sessions s
|
|
382
|
+
ON s.project_id = i.project_id
|
|
383
|
+
AND s.linear_issue_id = i.linear_issue_id
|
|
384
|
+
LEFT JOIN runs active_run ON active_run.id = i.active_run_id
|
|
385
|
+
LEFT JOIN runs latest_run ON latest_run.id = (
|
|
386
|
+
SELECT r.id FROM runs r
|
|
387
|
+
WHERE r.project_id = i.project_id AND r.linear_issue_id = i.linear_issue_id
|
|
388
|
+
ORDER BY r.id DESC LIMIT 1
|
|
389
|
+
)
|
|
390
|
+
${whereClause}
|
|
391
|
+
ORDER BY i.updated_at DESC, i.issue_key ASC
|
|
392
|
+
`)
|
|
393
|
+
.all(...values);
|
|
394
|
+
}
|
|
357
395
|
}
|
|
358
396
|
export function mapIssueRow(row) {
|
|
359
397
|
return {
|
|
@@ -170,6 +170,10 @@ export class LinearInstallationStore {
|
|
|
170
170
|
deleteLinearInstallation(installationId) {
|
|
171
171
|
this.connection.prepare("DELETE FROM linear_installations WHERE id = ?").run(installationId);
|
|
172
172
|
}
|
|
173
|
+
deleteCatalogForInstallation(installationId) {
|
|
174
|
+
this.connection.prepare("DELETE FROM linear_catalog_teams WHERE installation_id = ?").run(installationId);
|
|
175
|
+
this.connection.prepare("DELETE FROM linear_catalog_projects WHERE installation_id = ?").run(installationId);
|
|
176
|
+
}
|
|
173
177
|
getLinearInstallationForProject(projectId) {
|
|
174
178
|
const row = this.connection
|
|
175
179
|
.prepare(`
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every failure-provenance field on an issue, set to null.
|
|
3
|
+
*
|
|
4
|
+
* Spread into an issue upsert when a run reaches a clean advancing or terminal
|
|
5
|
+
* state (for example `awaiting_queue` or `done`) and any previously recorded
|
|
6
|
+
* GitHub failure or queue-incident context must not leak into the next
|
|
7
|
+
* decision. Keeping the field set in one place stops it from drifting between
|
|
8
|
+
* the run finalizer, the reconcilers, the completion check, and the webhook
|
|
9
|
+
* state projector.
|
|
10
|
+
*/
|
|
11
|
+
export const CLEARED_FAILURE_PROVENANCE = {
|
|
12
|
+
lastGitHubFailureSource: null,
|
|
13
|
+
lastGitHubFailureHeadSha: null,
|
|
14
|
+
lastGitHubFailureSignature: null,
|
|
15
|
+
lastGitHubFailureCheckName: null,
|
|
16
|
+
lastGitHubFailureCheckUrl: null,
|
|
17
|
+
lastGitHubFailureContextJson: null,
|
|
18
|
+
lastGitHubFailureAt: null,
|
|
19
|
+
lastQueueIncidentJson: null,
|
|
20
|
+
lastAttemptedFailureHeadSha: null,
|
|
21
|
+
lastAttemptedFailureSignature: null,
|
|
22
|
+
lastAttemptedFailureAt: null,
|
|
23
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CLEARED_FAILURE_PROVENANCE } from "./failure-provenance.js";
|
|
1
2
|
import { createGitHubCiSnapshotResolver, createGitHubFailureContextResolver } from "./github-failure-context.js";
|
|
2
3
|
import { buildClosedPrCleanupFields } from "./pr-state.js";
|
|
3
4
|
import { canClearFailureProvenance, deriveImmediatePrCheckStatus, getGateCheckNames, getPrimaryGateCheckName, isGateCheckEvent, isMetadataOnlyCheckEvent, isQueueEvictionFailure, isStaleGateEvent, isSettledBranchFailure, resolveGitHubFactoryStateForEvent, } from "./github-webhook-policy.js";
|
|
@@ -313,17 +314,7 @@ async function updateGitHubFailureProvenance(deps, issue, event, project, failur
|
|
|
313
314
|
deps.db.issues.upsertIssue({
|
|
314
315
|
projectId: issue.projectId,
|
|
315
316
|
linearIssueId: issue.linearIssueId,
|
|
316
|
-
|
|
317
|
-
lastGitHubFailureHeadSha: null,
|
|
318
|
-
lastGitHubFailureSignature: null,
|
|
319
|
-
lastGitHubFailureCheckName: null,
|
|
320
|
-
lastGitHubFailureCheckUrl: null,
|
|
321
|
-
lastGitHubFailureContextJson: null,
|
|
322
|
-
lastGitHubFailureAt: null,
|
|
323
|
-
lastQueueIncidentJson: null,
|
|
324
|
-
lastAttemptedFailureHeadSha: null,
|
|
325
|
-
lastAttemptedFailureSignature: null,
|
|
326
|
-
lastAttemptedFailureAt: null,
|
|
317
|
+
...CLEARED_FAILURE_PROVENANCE,
|
|
327
318
|
});
|
|
328
319
|
}
|
|
329
320
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TERMINAL_STATES } from "./factory-state.js";
|
|
2
|
+
import { CLEARED_FAILURE_PROVENANCE } from "./failure-provenance.js";
|
|
2
3
|
import { DEPLOY_WATCH_TIMEOUT_MS, evaluateDeploy, isDeployTrackingEnabled, } from "./post-merge-deploy.js";
|
|
3
4
|
import { buildBranchUpkeepContext, buildFailureContext, getGateCheckNames, hasCompletedReviewQuillVerdict, hasFailureProvenance, isDuplicateRepairAttempt, isFailingCheckStatus, isReviewDecisionApproved, isReviewDecisionReviewRequired, } from "./idle-reconciliation-helpers.js";
|
|
4
5
|
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
@@ -9,7 +10,7 @@ import { getReviewFixBudget } from "./run-budgets.js";
|
|
|
9
10
|
import { queueSettledOrchestrationIssue } from "./orchestration-parent-wake.js";
|
|
10
11
|
import { fetchPullRequestSnapshot } from "./reconcile-pr-fetch.js";
|
|
11
12
|
import { buildPrStateUpdates } from "./reconcile-pr-state-updates.js";
|
|
12
|
-
import { buildRequestedChangesWakeIdentity } from "./reactive-wake-keys.js";
|
|
13
|
+
import { buildRepairWakeDedupeKey, buildRequestedChangesWakeIdentity, reactiveWakeEventType } from "./reactive-wake-keys.js";
|
|
13
14
|
import { execCommand } from "./utils.js";
|
|
14
15
|
export class IdleIssueReconciler {
|
|
15
16
|
db;
|
|
@@ -239,19 +240,7 @@ export class IdleIssueReconciler {
|
|
|
239
240
|
}
|
|
240
241
|
: {}),
|
|
241
242
|
...(options?.clearFailureProvenance
|
|
242
|
-
? {
|
|
243
|
-
lastGitHubFailureSource: null,
|
|
244
|
-
lastGitHubFailureHeadSha: null,
|
|
245
|
-
lastGitHubFailureSignature: null,
|
|
246
|
-
lastGitHubFailureCheckName: null,
|
|
247
|
-
lastGitHubFailureCheckUrl: null,
|
|
248
|
-
lastGitHubFailureContextJson: null,
|
|
249
|
-
lastGitHubFailureAt: null,
|
|
250
|
-
lastQueueIncidentJson: null,
|
|
251
|
-
lastAttemptedFailureHeadSha: null,
|
|
252
|
-
lastAttemptedFailureSignature: null,
|
|
253
|
-
lastAttemptedFailureAt: null,
|
|
254
|
-
}
|
|
243
|
+
? { ...CLEARED_FAILURE_PROVENANCE }
|
|
255
244
|
: {}),
|
|
256
245
|
});
|
|
257
246
|
const updatedIssue = this.db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
@@ -277,18 +266,19 @@ export class IdleIssueReconciler {
|
|
|
277
266
|
// is needed here.
|
|
278
267
|
}
|
|
279
268
|
recordWakeEvent(issue, runType, context, dedupeScope = "idle_reconciliation") {
|
|
280
|
-
|
|
269
|
+
const eventType = reactiveWakeEventType(runType);
|
|
281
270
|
let dedupeKey;
|
|
282
|
-
if (runType === "queue_repair") {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
271
|
+
if (runType === "queue_repair" || runType === "ci_repair") {
|
|
272
|
+
dedupeKey = buildRepairWakeDedupeKey({
|
|
273
|
+
scope: dedupeScope,
|
|
274
|
+
runType,
|
|
275
|
+
linearIssueId: issue.linearIssueId,
|
|
276
|
+
signature: issue.lastGitHubFailureSignature,
|
|
277
|
+
prHeadSha: issue.prHeadSha,
|
|
278
|
+
failureHeadSha: issue.lastGitHubFailureHeadSha,
|
|
279
|
+
});
|
|
289
280
|
}
|
|
290
281
|
else if (runType === "review_fix" || runType === "branch_upkeep") {
|
|
291
|
-
eventType = "review_changes_requested";
|
|
292
282
|
dedupeKey = buildRequestedChangesWakeIdentity({
|
|
293
283
|
linearIssueId: issue.linearIssueId,
|
|
294
284
|
runType,
|
|
@@ -296,7 +286,6 @@ export class IdleIssueReconciler {
|
|
|
296
286
|
}).dedupeKey;
|
|
297
287
|
}
|
|
298
288
|
else {
|
|
299
|
-
eventType = "delegated";
|
|
300
289
|
dedupeKey = `${dedupeScope}:implementation:${issue.linearIssueId}`;
|
|
301
290
|
}
|
|
302
291
|
const requestedChangesIdentity = eventType === "review_changes_requested"
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { ACTIVE_RUN_STATES } from "./factory-state.js";
|
|
2
2
|
import { buildRunFailureActivity } from "./linear-session-reporting.js";
|
|
3
3
|
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
4
|
-
|
|
5
|
-
return runType === "review_fix" || runType === "branch_upkeep";
|
|
6
|
-
}
|
|
4
|
+
import { isRequestedChangesRunType } from "./reactive-pr-state.js";
|
|
7
5
|
function resolveRetryRunType(runType, context) {
|
|
8
6
|
if (runType === "branch_upkeep") {
|
|
9
7
|
return "branch_upkeep";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CLEARED_FAILURE_PROVENANCE } from "./failure-provenance.js";
|
|
1
2
|
import { buildCompletionCheckActivity } from "./linear-session-reporting.js";
|
|
2
3
|
import { wakeOrchestrationParentsForChildEvent } from "./orchestration-parent-wake.js";
|
|
3
4
|
function shouldContinueForUnpublishedLocalChanges(message) {
|
|
@@ -183,17 +184,7 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
183
184
|
pendingRunType: null,
|
|
184
185
|
pendingRunContextJson: null,
|
|
185
186
|
orchestrationSettleUntil: null,
|
|
186
|
-
|
|
187
|
-
lastGitHubFailureHeadSha: null,
|
|
188
|
-
lastGitHubFailureSignature: null,
|
|
189
|
-
lastGitHubFailureCheckName: null,
|
|
190
|
-
lastGitHubFailureCheckUrl: null,
|
|
191
|
-
lastGitHubFailureContextJson: null,
|
|
192
|
-
lastGitHubFailureAt: null,
|
|
193
|
-
lastQueueIncidentJson: null,
|
|
194
|
-
lastAttemptedFailureHeadSha: null,
|
|
195
|
-
lastAttemptedFailureSignature: null,
|
|
196
|
-
lastAttemptedFailureAt: null,
|
|
187
|
+
...CLEARED_FAILURE_PROVENANCE,
|
|
197
188
|
});
|
|
198
189
|
return true;
|
|
199
190
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
2
|
+
import { buildRepairWakeDedupeKey } from "./reactive-wake-keys.js";
|
|
2
3
|
import { execCommand } from "./utils.js";
|
|
3
4
|
const QUEUE_HEALTH_GRACE_MS = 120_000;
|
|
4
5
|
const QUEUE_HEALTH_PROBE_FAILURE_COOLDOWN_MS = 300_000;
|
|
@@ -167,7 +168,12 @@ export class QueueHealthMonitor {
|
|
|
167
168
|
this.advancer.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
|
|
168
169
|
eventType: "merge_steward_incident",
|
|
169
170
|
eventJson: JSON.stringify(pendingRunContext),
|
|
170
|
-
dedupeKey:
|
|
171
|
+
dedupeKey: buildRepairWakeDedupeKey({
|
|
172
|
+
scope: "queue_health",
|
|
173
|
+
runType: "queue_repair",
|
|
174
|
+
linearIssueId: issue.linearIssueId,
|
|
175
|
+
signature,
|
|
176
|
+
}),
|
|
171
177
|
});
|
|
172
178
|
this.advancer.advanceIdleIssue(issue, "repairing_queue");
|
|
173
179
|
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, headRefOid, reason }, "Queue health: queue issue detected, dispatching repair");
|
|
@@ -2,6 +2,15 @@ import { readRemotePrState } from "./remote-pr-state.js";
|
|
|
2
2
|
export function isRequestedChangesRunType(runType) {
|
|
3
3
|
return runType === "review_fix" || runType === "branch_upkeep";
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* The terminal state a failed run should land the issue in: requested-changes
|
|
7
|
+
* repairs escalate (a human asked for the change, so a silent failure must
|
|
8
|
+
* surface), everything else fails. Centralized so the mapping cannot diverge
|
|
9
|
+
* between the launcher and the notification handler.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveFailureFactoryState(runType) {
|
|
12
|
+
return isRequestedChangesRunType(runType) ? "escalated" : "failed";
|
|
13
|
+
}
|
|
5
14
|
export function normalizeRemotePrState(value) {
|
|
6
15
|
const normalized = value?.trim().toUpperCase();
|
|
7
16
|
if (normalized === "OPEN")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CLEARED_FAILURE_PROVENANCE } from "./failure-provenance.js";
|
|
1
2
|
import { buildReviewFixBranchUpkeepContext, isDirtyMergeStateStatus, isRequestedChangesRunType, readReactivePrSnapshot, } from "./reactive-pr-state.js";
|
|
2
3
|
import { readReactivePublishDelta } from "./reactive-publish-delta.js";
|
|
3
4
|
import { readLatestRequestedChangesReviewContext } from "./remote-pr-review.js";
|
|
@@ -173,17 +174,7 @@ export class ReactiveRunPolicy {
|
|
|
173
174
|
...((headAdvanced || reviewFixHeadAdvanced)
|
|
174
175
|
? {
|
|
175
176
|
prCheckStatus: "pending",
|
|
176
|
-
|
|
177
|
-
lastGitHubFailureHeadSha: null,
|
|
178
|
-
lastGitHubFailureSignature: null,
|
|
179
|
-
lastGitHubFailureCheckName: null,
|
|
180
|
-
lastGitHubFailureCheckUrl: null,
|
|
181
|
-
lastGitHubFailureContextJson: null,
|
|
182
|
-
lastGitHubFailureAt: null,
|
|
183
|
-
lastQueueIncidentJson: null,
|
|
184
|
-
lastAttemptedFailureHeadSha: null,
|
|
185
|
-
lastAttemptedFailureSignature: null,
|
|
186
|
-
lastAttemptedFailureAt: null,
|
|
177
|
+
...CLEARED_FAILURE_PROVENANCE,
|
|
187
178
|
lastGitHubCiSnapshotHeadSha: snapshot.headSha ?? null,
|
|
188
179
|
lastGitHubCiSnapshotGateCheckName: snapshot.gateCheckName,
|
|
189
180
|
lastGitHubCiSnapshotGateCheckStatus: "pending",
|
|
@@ -62,3 +62,32 @@ function parseObject(raw) {
|
|
|
62
62
|
return undefined;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Map a run type to the issue-session event type that wakes it. Shared by every
|
|
67
|
+
* reconciler that records a reactive wake (idle reconciliation, startup
|
|
68
|
+
* recovery, queue health) so the mapping stays in one place.
|
|
69
|
+
*/
|
|
70
|
+
export function reactiveWakeEventType(runType) {
|
|
71
|
+
switch (runType) {
|
|
72
|
+
case "queue_repair":
|
|
73
|
+
return "merge_steward_incident";
|
|
74
|
+
case "ci_repair":
|
|
75
|
+
return "settled_red_ci";
|
|
76
|
+
case "review_fix":
|
|
77
|
+
case "branch_upkeep":
|
|
78
|
+
return "review_changes_requested";
|
|
79
|
+
default:
|
|
80
|
+
return "delegated";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build the dedupe key for a CI/queue repair wake. The discriminator prefers the
|
|
85
|
+
* failure signature, then the PR head, then the recorded failure head, falling
|
|
86
|
+
* back to "unknown" — the same precedence every reconciler used independently
|
|
87
|
+
* before this was consolidated (a prior divergence here swallowed fresh repair
|
|
88
|
+
* incidents after the main branch advanced).
|
|
89
|
+
*/
|
|
90
|
+
export function buildRepairWakeDedupeKey(params) {
|
|
91
|
+
const discriminator = params.signature ?? params.prHeadSha ?? params.failureHeadSha ?? "unknown";
|
|
92
|
+
return `${params.scope}:${params.runType}:${params.linearIssueId}:${discriminator}`;
|
|
93
|
+
}
|
package/dist/run-finalizer.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CLEARED_FAILURE_PROVENANCE } from "./failure-provenance.js";
|
|
1
2
|
import { buildStageReport, countEventMethods } from "./run-reporting.js";
|
|
2
3
|
import { buildRunCompletedActivity, buildRunFailureActivity } from "./linear-session-reporting.js";
|
|
3
4
|
import { handleNoPrCompletionCheck } from "./no-pr-completion-check.js";
|
|
@@ -449,19 +450,7 @@ export class RunFinalizer {
|
|
|
449
450
|
pendingRunType: null,
|
|
450
451
|
pendingRunContextJson: null,
|
|
451
452
|
...(postRunFollowUp ? {} : (postRunState === "awaiting_queue" || postRunState === "done"
|
|
452
|
-
? {
|
|
453
|
-
lastGitHubFailureSource: null,
|
|
454
|
-
lastGitHubFailureHeadSha: null,
|
|
455
|
-
lastGitHubFailureSignature: null,
|
|
456
|
-
lastGitHubFailureCheckName: null,
|
|
457
|
-
lastGitHubFailureCheckUrl: null,
|
|
458
|
-
lastGitHubFailureContextJson: null,
|
|
459
|
-
lastGitHubFailureAt: null,
|
|
460
|
-
lastQueueIncidentJson: null,
|
|
461
|
-
lastAttemptedFailureHeadSha: null,
|
|
462
|
-
lastAttemptedFailureSignature: null,
|
|
463
|
-
lastAttemptedFailureAt: null,
|
|
464
|
-
}
|
|
453
|
+
? { ...CLEARED_FAILURE_PROVENANCE }
|
|
465
454
|
: {})),
|
|
466
455
|
});
|
|
467
456
|
if (postRunFollowUp) {
|
package/dist/run-launcher.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveFailureFactoryState } from "./reactive-pr-state.js";
|
|
1
2
|
import { buildHookEnv, runProjectHook } from "./hook-runner.js";
|
|
2
3
|
import { buildRunFailureActivity } from "./linear-session-reporting.js";
|
|
3
4
|
import { loadPatchRelayRepoPrompting } from "./patchrelay-customization.js";
|
|
@@ -228,7 +229,7 @@ export class RunLauncher {
|
|
|
228
229
|
const message = error instanceof Error ? error.message : String(error);
|
|
229
230
|
const lostLease = error instanceof Error && error.name === "IssueSessionLeaseLostError";
|
|
230
231
|
if (!lostLease) {
|
|
231
|
-
const nextState =
|
|
232
|
+
const nextState = resolveFailureFactoryState(params.runType);
|
|
232
233
|
this.db.issueSessions.finishRunWithLease({ projectId: params.project.id, linearIssueId: params.issue.linearIssueId, leaseId: params.leaseId }, params.run.id, {
|
|
233
234
|
status: "failed",
|
|
234
235
|
failureReason: message,
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { buildRunFailureActivity } from "./linear-session-reporting.js";
|
|
2
2
|
import { extractTurnId, resolveRunCompletionStatus } from "./run-reporting.js";
|
|
3
3
|
import { resolveRecoverablePostRunState } from "./interrupted-run-recovery.js";
|
|
4
|
-
|
|
5
|
-
return runType === "review_fix" || runType === "branch_upkeep";
|
|
6
|
-
}
|
|
4
|
+
import { resolveFailureFactoryState } from "./reactive-pr-state.js";
|
|
7
5
|
const DEFAULT_PUBLISH_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
8
6
|
export class RunNotificationHandler {
|
|
9
7
|
config;
|
|
@@ -91,7 +89,7 @@ export class RunNotificationHandler {
|
|
|
91
89
|
this.activeThreadId = undefined;
|
|
92
90
|
return;
|
|
93
91
|
}
|
|
94
|
-
const nextState =
|
|
92
|
+
const nextState = resolveFailureFactoryState(run.runType);
|
|
95
93
|
const updated = this.withHeldIssueSessionLease(run.projectId, run.linearIssueId, (lease) => {
|
|
96
94
|
this.db.issueSessions.finishRunWithLease(lease, run.id, {
|
|
97
95
|
status: "failed",
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRequestedChangesRunType } from "./reactive-pr-state.js";
|
|
1
2
|
import { summarizeCurrentThread } from "./run-reporting.js";
|
|
2
3
|
import { buildReviewRoundStartedActivity, buildRunStartedActivity, } from "./linear-session-reporting.js";
|
|
3
4
|
import { CompletionCheckService } from "./completion-check.js";
|
|
@@ -25,9 +26,6 @@ import { CodexThreadMaterializingError, isThreadMaterializingError } from "./cod
|
|
|
25
26
|
function lowerCaseFirst(value) {
|
|
26
27
|
return value ? `${value.slice(0, 1).toLowerCase()}${value.slice(1)}` : value;
|
|
27
28
|
}
|
|
28
|
-
function isRequestedChangesRunType(runType) {
|
|
29
|
-
return runType === "review_fix" || runType === "branch_upkeep";
|
|
30
|
-
}
|
|
31
29
|
function shouldDelayZombieRecoveryLaunch(issue, issueSession, runType) {
|
|
32
30
|
if (issue.zombieRecoveryAttempts <= 0)
|
|
33
31
|
return 0;
|
|
@@ -420,7 +418,6 @@ export class RunOrchestrator {
|
|
|
420
418
|
assertLaunchLease: (targetRun, phase) => this.assertLaunchLease(targetRun, phase),
|
|
421
419
|
linearSync: this.linearSync,
|
|
422
420
|
releaseLease: (projectId, issueId) => this.releaseIssueSessionLease(projectId, issueId),
|
|
423
|
-
isRequestedChangesRunType,
|
|
424
421
|
lowerCaseFirst,
|
|
425
422
|
});
|
|
426
423
|
this.assertLaunchLease(run, "before recording the active thread");
|
|
@@ -1,7 +1,7 @@
|
|
|
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 { buildRequestedChangesWakeIdentity } from "./reactive-wake-keys.js";
|
|
4
|
+
import { buildRepairWakeDedupeKey, buildRequestedChangesWakeIdentity, reactiveWakeEventType } from "./reactive-wake-keys.js";
|
|
5
5
|
export class ServiceStartupRecovery {
|
|
6
6
|
db;
|
|
7
7
|
linearProvider;
|
|
@@ -170,20 +170,21 @@ export class ServiceStartupRecovery {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
appendReactiveWakeEvent(projectId, linearIssueId, issue, runType) {
|
|
173
|
-
const eventType = runType
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
173
|
+
const eventType = reactiveWakeEventType(runType);
|
|
174
|
+
const dedupeKey = runType === "queue_repair" || runType === "ci_repair"
|
|
175
|
+
? buildRepairWakeDedupeKey({
|
|
176
|
+
scope: "startup_recovery",
|
|
177
|
+
runType,
|
|
178
|
+
linearIssueId,
|
|
179
|
+
signature: issue.lastGitHubFailureSignature,
|
|
180
|
+
prHeadSha: issue.prHeadSha,
|
|
181
|
+
failureHeadSha: issue.lastGitHubFailureHeadSha,
|
|
182
|
+
})
|
|
183
|
+
: buildRequestedChangesWakeIdentity({
|
|
184
|
+
linearIssueId,
|
|
185
|
+
runType,
|
|
186
|
+
headSha: issue.prHeadSha,
|
|
187
|
+
}).dedupeKey;
|
|
187
188
|
const requestedChangesIdentity = eventType === "review_changes_requested"
|
|
188
189
|
? buildRequestedChangesWakeIdentity({
|
|
189
190
|
linearIssueId,
|
package/dist/service.js
CHANGED
|
@@ -261,8 +261,7 @@ export class PatchRelayService {
|
|
|
261
261
|
}
|
|
262
262
|
this.db.transaction(() => {
|
|
263
263
|
this.db.linearInstallations.unlinkInstallationProjects(installation.id);
|
|
264
|
-
this.db.
|
|
265
|
-
this.db.connection.prepare("DELETE FROM linear_catalog_projects WHERE installation_id = ?").run(installation.id);
|
|
264
|
+
this.db.linearInstallations.deleteCatalogForInstallation(installation.id);
|
|
266
265
|
this.db.linearInstallations.deleteLinearInstallation(installation.id);
|
|
267
266
|
});
|
|
268
267
|
return {
|
|
@@ -78,76 +78,7 @@ export class TrackedIssueListQuery {
|
|
|
78
78
|
this.db = db;
|
|
79
79
|
}
|
|
80
80
|
listTrackedIssues() {
|
|
81
|
-
const rows = this.db.
|
|
82
|
-
.prepare(`SELECT
|
|
83
|
-
s.project_id, s.linear_issue_id, s.issue_key, i.title,
|
|
84
|
-
i.current_linear_state, i.factory_state, i.delegated_to_patchrelay, s.session_state, s.waiting_reason, s.summary_text, s.display_updated_at,
|
|
85
|
-
i.pending_run_type,
|
|
86
|
-
i.orchestration_settle_until,
|
|
87
|
-
i.pr_number, i.pr_state, i.pr_head_sha, i.pr_review_state, i.pr_check_status, i.last_blocking_review_head_sha,
|
|
88
|
-
i.last_github_ci_snapshot_json,
|
|
89
|
-
i.last_github_failure_source,
|
|
90
|
-
i.last_github_failure_head_sha,
|
|
91
|
-
i.last_github_failure_check_name,
|
|
92
|
-
i.last_github_failure_context_json,
|
|
93
|
-
active_run.run_type AS active_run_type,
|
|
94
|
-
active_run.completion_check_thread_id AS active_completion_check_thread_id,
|
|
95
|
-
active_run.completion_check_outcome AS active_completion_check_outcome,
|
|
96
|
-
latest_run.run_type AS latest_run_type,
|
|
97
|
-
latest_run.status AS latest_run_status,
|
|
98
|
-
latest_run.summary_json AS latest_run_summary_json,
|
|
99
|
-
latest_run.report_json AS latest_run_report_json,
|
|
100
|
-
latest_run.completion_check_thread_id AS latest_run_completion_check_thread_id,
|
|
101
|
-
latest_run.completion_check_outcome AS latest_run_completion_check_outcome,
|
|
102
|
-
latest_run.completion_check_summary AS latest_run_completion_check_summary,
|
|
103
|
-
latest_run.completion_check_question AS latest_run_completion_check_question,
|
|
104
|
-
latest_run.completion_check_why AS latest_run_completion_check_why,
|
|
105
|
-
latest_run.completion_check_recommended_reply AS latest_run_completion_check_recommended_reply,
|
|
106
|
-
(
|
|
107
|
-
SELECT COUNT(*)
|
|
108
|
-
FROM issue_session_events e
|
|
109
|
-
WHERE e.project_id = s.project_id
|
|
110
|
-
AND e.linear_issue_id = s.linear_issue_id
|
|
111
|
-
AND e.processed_at IS NULL
|
|
112
|
-
) AS pending_session_event_count,
|
|
113
|
-
(
|
|
114
|
-
SELECT COUNT(*)
|
|
115
|
-
FROM issue_dependencies d
|
|
116
|
-
LEFT JOIN issues blockers
|
|
117
|
-
ON blockers.project_id = d.project_id
|
|
118
|
-
AND blockers.linear_issue_id = d.blocker_linear_issue_id
|
|
119
|
-
WHERE d.project_id = s.project_id
|
|
120
|
-
AND d.linear_issue_id = s.linear_issue_id
|
|
121
|
-
AND (
|
|
122
|
-
COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
|
|
123
|
-
AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
|
|
124
|
-
)
|
|
125
|
-
) AS blocked_by_count,
|
|
126
|
-
(
|
|
127
|
-
SELECT json_group_array(COALESCE(blockers.issue_key, d.blocker_issue_key, d.blocker_linear_issue_id))
|
|
128
|
-
FROM issue_dependencies d
|
|
129
|
-
LEFT JOIN issues blockers
|
|
130
|
-
ON blockers.project_id = d.project_id
|
|
131
|
-
AND blockers.linear_issue_id = d.blocker_linear_issue_id
|
|
132
|
-
WHERE d.project_id = s.project_id
|
|
133
|
-
AND d.linear_issue_id = s.linear_issue_id
|
|
134
|
-
AND (
|
|
135
|
-
COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
|
|
136
|
-
AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
|
|
137
|
-
)
|
|
138
|
-
) AS blocked_by_keys_json
|
|
139
|
-
FROM issue_sessions s
|
|
140
|
-
LEFT JOIN issues i
|
|
141
|
-
ON i.project_id = s.project_id
|
|
142
|
-
AND i.linear_issue_id = s.linear_issue_id
|
|
143
|
-
LEFT JOIN runs active_run ON active_run.id = COALESCE(s.active_run_id, i.active_run_id)
|
|
144
|
-
LEFT JOIN runs latest_run ON latest_run.id = (
|
|
145
|
-
SELECT r.id FROM runs r
|
|
146
|
-
WHERE r.project_id = s.project_id AND r.linear_issue_id = s.linear_issue_id
|
|
147
|
-
ORDER BY r.id DESC LIMIT 1
|
|
148
|
-
)
|
|
149
|
-
ORDER BY s.display_updated_at DESC, s.issue_key ASC`)
|
|
150
|
-
.all();
|
|
81
|
+
const rows = this.db.issueSessions.listTrackedIssueRows();
|
|
151
82
|
return rows.map((row) => {
|
|
152
83
|
const failureContext = parseGitHubFailureContext(typeof row.last_github_failure_context_json === "string" ? row.last_github_failure_context_json : undefined);
|
|
153
84
|
const prChecksSummary = parseCiSnapshotSummary(typeof row.last_github_ci_snapshot_json === "string" ? row.last_github_ci_snapshot_json : undefined);
|