patchrelay 0.35.0 → 0.35.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.35.0",
4
- "commit": "e1a5b7d3b9e0",
5
- "builtAt": "2026-04-02T21:55:42.542Z"
3
+ "version": "0.35.1",
4
+ "commit": "f9a5d8a6d7ff",
5
+ "builtAt": "2026-04-02T23:43:16.960Z"
6
6
  }
@@ -1,31 +1,35 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- function statusColor(status) {
4
- switch (status) {
5
- case "current":
6
- return "cyan";
7
- case "visited":
8
- return "green";
9
- case "upcoming":
10
- return "gray";
11
- }
3
+ const STATE_LABELS = {
4
+ delegated: "delegated",
5
+ implementing: "implementing",
6
+ pr_open: "PR open",
7
+ awaiting_queue: "merge queue",
8
+ done: "done",
9
+ changes_requested: "review fix",
10
+ repairing_ci: "CI repair",
11
+ repairing_queue: "queue repair",
12
+ awaiting_input: "needs input",
13
+ escalated: "escalated",
14
+ failed: "failed",
15
+ };
16
+ function displayLabel(state) {
17
+ return STATE_LABELS[state] ?? state;
12
18
  }
13
- function statusPrefix(status) {
14
- switch (status) {
15
- case "current":
16
- return "*";
17
- case "visited":
18
- return "+";
19
- case "upcoming":
20
- return " ";
21
- }
22
- }
23
- function NodePill({ node }) {
24
- return (_jsxs(Text, { color: statusColor(node.status), bold: node.status === "current", children: ["[", statusPrefix(node.status), " ", node.label, "]"] }));
25
- }
26
- function NodeRow({ label, nodes, connector = " -> ", }) {
27
- return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: label.padEnd(11, " ") }), nodes.map((node, index) => (_jsxs(Box, { children: [index > 0 && _jsx(Text, { dimColor: true, children: connector }), _jsx(NodePill, { node: node })] }, node.state)))] }));
19
+ function NodeRow({ nodes, connector }) {
20
+ const visible = nodes.filter((n) => connector === " \u2192 " || n.status !== "upcoming");
21
+ if (visible.length === 0)
22
+ return _jsx(_Fragment, {});
23
+ return (_jsx(Box, { gap: 0, children: visible.map((node, i) => {
24
+ const dot = node.status === "upcoming" ? "\u25cb" : "\u25cf";
25
+ const color = node.status === "current" ? "cyan"
26
+ : node.status === "visited" ? "green"
27
+ : "gray";
28
+ return (_jsxs(Box, { gap: 0, children: [i > 0 && _jsx(Text, { dimColor: true, children: connector }), _jsx(Text, { color: color, bold: node.status === "current", children: dot }), _jsx(Text, { color: color, bold: node.status === "current", children: ` ${displayLabel(node.state)}` })] }, node.state));
29
+ }) }));
28
30
  }
29
31
  export function FactoryStateGraph({ main, prLoops, queueLoop, exits, }) {
30
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "State Graph" }), _jsx(NodeRow, { label: "main", nodes: main }), _jsx(NodeRow, { label: "pr loops", nodes: prLoops, connector: " " }), _jsx(NodeRow, { label: "queue loop", nodes: queueLoop, connector: " " }), _jsx(NodeRow, { label: "exits", nodes: exits, connector: " " })] }));
32
+ const hasLoops = prLoops.some((n) => n.status !== "upcoming") || queueLoop.some((n) => n.status !== "upcoming");
33
+ const hasExits = exits.some((n) => n.status !== "upcoming");
34
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(NodeRow, { nodes: main, connector: " \\u2192 " }), hasLoops && (_jsx(Box, { gap: 0, paddingLeft: 2, children: _jsx(NodeRow, { nodes: [...prLoops, ...queueLoop], connector: " " }) })), hasExits && (_jsx(Box, { gap: 0, paddingLeft: 2, children: _jsx(NodeRow, { nodes: exits, connector: " " }) }))] }));
31
35
  }
@@ -9,12 +9,11 @@ export function Timeline({ entries, follow }) {
9
9
  const rows = stdout?.rows ?? 24;
10
10
  const maxActive = Math.max(ACTIVE_TAIL, rows - 12);
11
11
  const displayRows = useMemo(() => buildTimelineRows(entries), [entries]);
12
- const splitIndex = useMemo(() => {
13
- if (!follow)
14
- return 0;
15
- return Math.max(0, displayRows.length - maxActive);
16
- }, [displayRows.length, follow, maxActive]);
17
- const finalized = displayRows.slice(0, splitIndex);
12
+ // Always cap the rendered entries to prevent OOM/WASM crashes.
13
+ // In follow mode: older entries go to Static (terminal scrollback).
14
+ // Without follow: show last maxActive entries only.
15
+ const splitIndex = Math.max(0, displayRows.length - maxActive);
16
+ const finalized = follow ? displayRows.slice(0, splitIndex) : [];
18
17
  const active = displayRows.slice(splitIndex);
19
18
  if (displayRows.length === 0) {
20
19
  return _jsx(Text, { dimColor: true, children: "No timeline events yet." });
@@ -632,15 +632,31 @@ export class RunOrchestrator {
632
632
  const hasQueueLabel = pr.labels?.some((l) => l.name === protocol.admissionLabel) ?? false;
633
633
  if (!hasQueueLabel)
634
634
  return;
635
- // Conflict detected dispatch preemptive queue repair.
636
- if (pr.mergeStateStatus === "DIRTY" || pr.mergeable === "CONFLICTING") {
635
+ // Detect queue issues: either GitHub reports DIRTY, or the steward
636
+ // eviction check run failed (webhook may have been missed).
637
+ const isDirty = pr.mergeStateStatus === "DIRTY" || pr.mergeable === "CONFLICTING";
638
+ let hasEvictionCheckRun = false;
639
+ if (!isDirty) {
640
+ // Check for missed eviction webhook by looking for the steward's
641
+ // check run on the PR head.
642
+ try {
643
+ const { stdout: checksOut } = await execCommand("gh", [
644
+ "api", `repos/${project.github.repoFullName}/commits/${pr.headRefOid}/check-runs`,
645
+ "--jq", `.check_runs[] | select(.name == "${protocol.evictionCheckName}" and .conclusion == "failure") | .name`,
646
+ ], { timeoutMs: 10_000 });
647
+ hasEvictionCheckRun = checksOut.trim().length > 0;
648
+ }
649
+ catch {
650
+ // Best-effort check.
651
+ }
652
+ }
653
+ if (isDirty || hasEvictionCheckRun) {
637
654
  const headRefOid = pr.headRefOid ?? "unknown";
638
- // TODO: include baseSha in signature (headRefOid + baseSha) so that a
639
- // main-only advance with the same PR head is recognized as a new conflict.
655
+ const reason = hasEvictionCheckRun ? "queue_eviction_missed" : "preemptive_conflict";
640
656
  const signature = `preemptive_queue_conflict:${headRefOid}`;
641
657
  const pendingRunContext = {
642
658
  source: "queue_health_monitor",
643
- failureReason: "preemptive_conflict",
659
+ failureReason: reason,
644
660
  failureHeadSha: headRefOid,
645
661
  failureSignature: signature,
646
662
  };
@@ -657,15 +673,17 @@ export class RunOrchestrator {
657
673
  pendingRunType: "queue_repair",
658
674
  pendingRunContext,
659
675
  });
660
- this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, headRefOid }, "Queue health: merge conflict detected, dispatching preemptive repair");
676
+ this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, headRefOid, reason }, "Queue health: queue issue detected, dispatching repair");
661
677
  this.feed?.publish({
662
678
  level: "warn",
663
679
  kind: "github",
664
680
  issueKey: issue.issueKey,
665
681
  projectId: issue.projectId,
666
682
  stage: "repairing_queue",
667
- status: "queue_health_conflict_detected",
668
- summary: `Queue health: merge conflict detected on PR #${issue.prNumber}, dispatching preemptive repair`,
683
+ status: hasEvictionCheckRun ? "queue_health_eviction_detected" : "queue_health_conflict_detected",
684
+ summary: hasEvictionCheckRun
685
+ ? `Queue health: missed eviction detected on PR #${issue.prNumber}, dispatching repair`
686
+ : `Queue health: merge conflict detected on PR #${issue.prNumber}, dispatching preemptive repair`,
669
687
  });
670
688
  }
671
689
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {