patchrelay 0.35.10 → 0.35.12
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/README.md +41 -9
- package/dist/build-info.json +3 -3
- package/dist/cli/args.js +0 -1
- package/dist/cli/commands/issues.js +2 -56
- package/dist/cli/commands/watch.js +5 -0
- package/dist/cli/data.js +110 -47
- package/dist/cli/formatters/text.js +6 -90
- package/dist/cli/help.js +3 -8
- package/dist/cli/index.js +0 -48
- package/dist/cli/operator-client.js +0 -82
- package/dist/cli/watch/App.js +1 -12
- package/dist/cli/watch/HelpBar.js +2 -2
- package/dist/cli/watch/IssueDetailView.js +57 -26
- package/dist/cli/watch/IssueRow.js +71 -27
- package/dist/cli/watch/StatusBar.js +7 -4
- package/dist/cli/watch/state-visualization.js +48 -23
- package/dist/cli/watch/timeline-builder.js +2 -1
- package/dist/cli/watch/use-detail-stream.js +10 -104
- package/dist/cli/watch/use-watch-stream.js +11 -102
- package/dist/cli/watch/watch-state.js +18 -50
- package/dist/codex-thread-utils.js +3 -0
- package/dist/db/migrations.js +239 -2
- package/dist/db.js +628 -39
- package/dist/github-app-token.js +7 -0
- package/dist/github-failure-context.js +44 -1
- package/dist/github-rollup.js +47 -0
- package/dist/github-webhook-handler.js +248 -51
- package/dist/github-webhooks.js +5 -0
- package/dist/http.js +12 -264
- package/dist/idle-reconciliation.js +275 -74
- package/dist/issue-query-service.js +221 -129
- package/dist/issue-session-events.js +151 -0
- package/dist/issue-session.js +99 -0
- package/dist/linear-client.js +39 -25
- package/dist/linear-session-reporting.js +12 -0
- package/dist/linear-session-sync.js +253 -24
- package/dist/linear-workflow.js +33 -0
- package/dist/merge-queue-protocol.js +0 -51
- package/dist/preflight.js +1 -4
- package/dist/queue-health-monitor.js +11 -7
- package/dist/run-orchestrator.js +1295 -146
- package/dist/run-reporting.js +5 -3
- package/dist/service.js +279 -102
- package/dist/status-note.js +56 -0
- package/dist/waiting-reason.js +65 -0
- package/dist/webhook-handler.js +270 -79
- package/package.json +1 -1
- package/dist/cli/commands/feed.js +0 -60
- package/dist/cli/watch/FeedView.js +0 -28
- package/dist/cli/watch/use-feed-stream.js +0 -92
|
@@ -16,9 +16,6 @@ const PR_LOOP_STATES = ["changes_requested", "repairing_ci"];
|
|
|
16
16
|
const QUEUE_LOOP_STATES = ["repairing_queue"];
|
|
17
17
|
const EXIT_STATES = ["awaiting_input", "escalated", "failed"];
|
|
18
18
|
const QUEUE_EVENT_STATUSES = new Set([
|
|
19
|
-
"queue_label_requested",
|
|
20
|
-
"queue_label_applied",
|
|
21
|
-
"queue_label_failed",
|
|
22
19
|
"queue_repair_queued",
|
|
23
20
|
"pr_merged",
|
|
24
21
|
]);
|
|
@@ -61,12 +58,6 @@ function latestQueueObservationEvent(feedEvents) {
|
|
|
61
58
|
}
|
|
62
59
|
function describeObservationEvent(event) {
|
|
63
60
|
switch (event.status) {
|
|
64
|
-
case "queue_label_requested":
|
|
65
|
-
return { tone: "info", text: event.summary };
|
|
66
|
-
case "queue_label_applied":
|
|
67
|
-
return { tone: "success", text: event.summary };
|
|
68
|
-
case "queue_label_failed":
|
|
69
|
-
return { tone: "warn", text: event.summary };
|
|
70
61
|
case "queue_repair_queued":
|
|
71
62
|
return { tone: "warn", text: event.summary };
|
|
72
63
|
case "pr_merged":
|
|
@@ -82,10 +73,10 @@ function describeObservationEvent(event) {
|
|
|
82
73
|
const active = event.status === "starting";
|
|
83
74
|
return {
|
|
84
75
|
tone: active ? "warn" : "info",
|
|
85
|
-
text: active ? "PatchRelay is actively running queue repair." : event.summary
|
|
76
|
+
text: active ? "PatchRelay is actively running queue repair." : `Observed queue signal: ${event.summary}`,
|
|
86
77
|
};
|
|
87
78
|
}
|
|
88
|
-
return { tone: "info", text: event.summary };
|
|
79
|
+
return { tone: "info", text: `Observed signal: ${event.summary}` };
|
|
89
80
|
}
|
|
90
81
|
}
|
|
91
82
|
export function buildPatchRelayStateGraph(history, currentFactoryState) {
|
|
@@ -99,19 +90,23 @@ export function buildPatchRelayStateGraph(history, currentFactoryState) {
|
|
|
99
90
|
}
|
|
100
91
|
export function buildPatchRelayQueueObservations(issue, feedEvents) {
|
|
101
92
|
const observations = [];
|
|
102
|
-
switch (issue.
|
|
103
|
-
case "
|
|
93
|
+
switch (issue.sessionState) {
|
|
94
|
+
case "waiting_input":
|
|
95
|
+
observations.push({
|
|
96
|
+
tone: "warn",
|
|
97
|
+
text: issue.waitingReason ?? "PatchRelay is waiting for input before continuing.",
|
|
98
|
+
});
|
|
99
|
+
break;
|
|
100
|
+
case "running":
|
|
104
101
|
observations.push({
|
|
105
102
|
tone: "info",
|
|
106
|
-
text: "PatchRelay
|
|
103
|
+
text: "PatchRelay is actively working this session.",
|
|
107
104
|
});
|
|
108
105
|
break;
|
|
109
|
-
case "
|
|
106
|
+
case "idle":
|
|
110
107
|
observations.push({
|
|
111
|
-
tone:
|
|
112
|
-
text: issue.
|
|
113
|
-
? "PatchRelay is actively repairing a queue eviction."
|
|
114
|
-
: "PatchRelay is preparing or waiting to resume queue repair.",
|
|
108
|
+
tone: "info",
|
|
109
|
+
text: "PatchRelay is idle for this issue.",
|
|
115
110
|
});
|
|
116
111
|
break;
|
|
117
112
|
case "done":
|
|
@@ -120,12 +115,42 @@ export function buildPatchRelayQueueObservations(issue, feedEvents) {
|
|
|
120
115
|
text: "PatchRelay is complete because GitHub reports the PR has merged.",
|
|
121
116
|
});
|
|
122
117
|
break;
|
|
123
|
-
|
|
118
|
+
case "failed":
|
|
124
119
|
observations.push({
|
|
125
|
-
tone: "
|
|
126
|
-
text: "
|
|
120
|
+
tone: "warn",
|
|
121
|
+
text: "PatchRelay needs human help to recover this session.",
|
|
127
122
|
});
|
|
128
123
|
break;
|
|
124
|
+
default:
|
|
125
|
+
switch (issue.factoryState) {
|
|
126
|
+
case "awaiting_queue":
|
|
127
|
+
observations.push({
|
|
128
|
+
tone: "info",
|
|
129
|
+
text: "PatchRelay has finished active work and is waiting for downstream merge flow.",
|
|
130
|
+
});
|
|
131
|
+
break;
|
|
132
|
+
case "repairing_queue":
|
|
133
|
+
observations.push({
|
|
134
|
+
tone: issue.activeRunType === "queue_repair" ? "warn" : "info",
|
|
135
|
+
text: issue.activeRunType === "queue_repair"
|
|
136
|
+
? "PatchRelay is actively repairing a queue eviction."
|
|
137
|
+
: "PatchRelay is preparing or waiting to resume queue repair.",
|
|
138
|
+
});
|
|
139
|
+
break;
|
|
140
|
+
case "done":
|
|
141
|
+
observations.push({
|
|
142
|
+
tone: "success",
|
|
143
|
+
text: "PatchRelay is complete because GitHub reports the PR has merged.",
|
|
144
|
+
});
|
|
145
|
+
break;
|
|
146
|
+
default:
|
|
147
|
+
observations.push({
|
|
148
|
+
tone: "info",
|
|
149
|
+
text: "PatchRelay is tracking this issue.",
|
|
150
|
+
});
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
129
154
|
}
|
|
130
155
|
const latestEvent = latestQueueObservationEvent(feedEvents);
|
|
131
156
|
if (latestEvent) {
|
|
@@ -134,7 +159,7 @@ export function buildPatchRelayQueueObservations(issue, feedEvents) {
|
|
|
134
159
|
else if (issue.factoryState === "awaiting_queue") {
|
|
135
160
|
observations.push({
|
|
136
161
|
tone: "info",
|
|
137
|
-
text: "No
|
|
162
|
+
text: "No downstream queue signal has been observed yet.",
|
|
138
163
|
});
|
|
139
164
|
}
|
|
140
165
|
if (issue.prNumber !== undefined) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getThreadTurns } from "../../codex-thread-utils.js";
|
|
1
2
|
// ─── Build Timeline from Rehydration Data ─────────────────────────
|
|
2
3
|
export function buildTimelineFromRehydration(runs, feedEvents, liveThread, activeRunId) {
|
|
3
4
|
const entries = [];
|
|
@@ -132,7 +133,7 @@ function syntheticTimestamp(startMs, endMs, index, total) {
|
|
|
132
133
|
// ─── Items from Live Thread ───────────────────────────────────────
|
|
133
134
|
function itemsFromThread(runId, thread) {
|
|
134
135
|
const entries = [];
|
|
135
|
-
for (const turn of thread
|
|
136
|
+
for (const turn of getThreadTurns(thread)) {
|
|
136
137
|
for (const item of turn.items) {
|
|
137
138
|
entries.push({
|
|
138
139
|
id: `live-${item.id}`,
|
|
@@ -14,11 +14,12 @@ export function useDetailStream(options) {
|
|
|
14
14
|
if (bearerToken) {
|
|
15
15
|
headers.authorization = `Bearer ${bearerToken}`;
|
|
16
16
|
}
|
|
17
|
-
// Rehydrate from timeline endpoint
|
|
18
17
|
void rehydrate(baseUrl, issueKey, headers, abortController.signal, dispatch);
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const intervalId = setInterval(() => {
|
|
19
|
+
void rehydrate(baseUrl, issueKey, headers, abortController.signal, dispatch);
|
|
20
|
+
}, 3000);
|
|
21
21
|
return () => {
|
|
22
|
+
clearInterval(intervalId);
|
|
22
23
|
abortController.abort();
|
|
23
24
|
};
|
|
24
25
|
}, [options.issueKey, options.active]);
|
|
@@ -26,117 +27,22 @@ export function useDetailStream(options) {
|
|
|
26
27
|
// ─── Rehydration ──────────────────────────────────────────────────
|
|
27
28
|
async function rehydrate(baseUrl, issueKey, headers, signal, dispatch) {
|
|
28
29
|
try {
|
|
29
|
-
const url = new URL(`/api/issues/${encodeURIComponent(issueKey)}
|
|
30
|
+
const url = new URL(`/api/issues/${encodeURIComponent(issueKey)}`, baseUrl);
|
|
30
31
|
const response = await fetch(url, { headers: { ...headers, accept: "application/json" }, signal });
|
|
31
32
|
if (!response.ok)
|
|
32
33
|
return;
|
|
33
34
|
const data = await response.json();
|
|
34
|
-
const runs = (data.runs ?? []).map((r) => ({
|
|
35
|
-
id: r.id,
|
|
36
|
-
runType: r.runType,
|
|
37
|
-
status: r.status,
|
|
38
|
-
startedAt: r.startedAt,
|
|
39
|
-
endedAt: r.endedAt,
|
|
40
|
-
threadId: r.threadId,
|
|
41
|
-
...(r.events ? { events: r.events } : {}),
|
|
42
|
-
...(r.report ? { report: r.report } : {}),
|
|
43
|
-
}));
|
|
44
|
-
let issueContext = null;
|
|
45
|
-
if (data.issue) {
|
|
46
|
-
const i = data.issue;
|
|
47
|
-
issueContext = {
|
|
48
|
-
description: typeof i.description === "string" ? i.description : undefined,
|
|
49
|
-
currentLinearState: typeof i.currentLinearState === "string" ? i.currentLinearState : undefined,
|
|
50
|
-
issueUrl: typeof i.issueUrl === "string" ? i.issueUrl : undefined,
|
|
51
|
-
worktreePath: typeof i.worktreePath === "string" ? i.worktreePath : undefined,
|
|
52
|
-
branchName: typeof i.branchName === "string" ? i.branchName : undefined,
|
|
53
|
-
prUrl: typeof i.prUrl === "string" ? i.prUrl : undefined,
|
|
54
|
-
priority: typeof i.priority === "number" ? i.priority : undefined,
|
|
55
|
-
estimate: typeof i.estimate === "number" ? i.estimate : undefined,
|
|
56
|
-
ciRepairAttempts: typeof i.ciRepairAttempts === "number" ? i.ciRepairAttempts : 0,
|
|
57
|
-
queueRepairAttempts: typeof i.queueRepairAttempts === "number" ? i.queueRepairAttempts : 0,
|
|
58
|
-
reviewFixAttempts: typeof i.reviewFixAttempts === "number" ? i.reviewFixAttempts : 0,
|
|
59
|
-
latestFailureSource: typeof i.latestFailureSource === "string" ? i.latestFailureSource : undefined,
|
|
60
|
-
latestFailureHeadSha: typeof i.latestFailureHeadSha === "string" ? i.latestFailureHeadSha : undefined,
|
|
61
|
-
latestFailureCheckName: typeof i.latestFailureCheckName === "string" ? i.latestFailureCheckName : undefined,
|
|
62
|
-
latestFailureStepName: typeof i.latestFailureStepName === "string" ? i.latestFailureStepName : undefined,
|
|
63
|
-
latestFailureSummary: typeof i.latestFailureSummary === "string" ? i.latestFailureSummary : undefined,
|
|
64
|
-
runCount: runs.length,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
35
|
dispatch({
|
|
68
36
|
type: "timeline-rehydrate",
|
|
69
|
-
runs,
|
|
70
|
-
feedEvents:
|
|
37
|
+
runs: Array.isArray(data.runs) ? data.runs : [],
|
|
38
|
+
feedEvents: [],
|
|
71
39
|
liveThread: data.liveThread ?? null,
|
|
72
|
-
activeRunId: data.
|
|
73
|
-
|
|
40
|
+
activeRunId: data.activeRun?.id ?? null,
|
|
41
|
+
activeRunStartedAt: data.activeRun?.startedAt ?? null,
|
|
42
|
+
issueContext: data.issueContext ?? null,
|
|
74
43
|
});
|
|
75
44
|
}
|
|
76
45
|
catch {
|
|
77
46
|
// Rehydration is best-effort
|
|
78
47
|
}
|
|
79
48
|
}
|
|
80
|
-
// ─── Live SSE Stream ──────────────────────────────────────────────
|
|
81
|
-
async function streamEvents(baseUrl, issueKey, baseHeaders, signal, dispatch) {
|
|
82
|
-
try {
|
|
83
|
-
const url = new URL("/api/watch", baseUrl);
|
|
84
|
-
url.searchParams.set("issue", issueKey);
|
|
85
|
-
const headers = { ...baseHeaders, accept: "text/event-stream" };
|
|
86
|
-
const response = await fetch(url, { headers, signal });
|
|
87
|
-
if (!response.ok || !response.body)
|
|
88
|
-
return;
|
|
89
|
-
const reader = response.body.getReader();
|
|
90
|
-
const decoder = new TextDecoder();
|
|
91
|
-
let buffer = "";
|
|
92
|
-
let eventType = "";
|
|
93
|
-
let dataLines = [];
|
|
94
|
-
while (true) {
|
|
95
|
-
const { done, value } = await reader.read();
|
|
96
|
-
if (done)
|
|
97
|
-
break;
|
|
98
|
-
buffer += decoder.decode(value, { stream: true });
|
|
99
|
-
let newlineIndex = buffer.indexOf("\n");
|
|
100
|
-
while (newlineIndex !== -1) {
|
|
101
|
-
const rawLine = buffer.slice(0, newlineIndex);
|
|
102
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
103
|
-
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
104
|
-
if (!line) {
|
|
105
|
-
if (dataLines.length > 0) {
|
|
106
|
-
processEvent(dispatch, eventType, dataLines.join("\n"));
|
|
107
|
-
dataLines = [];
|
|
108
|
-
eventType = "";
|
|
109
|
-
}
|
|
110
|
-
newlineIndex = buffer.indexOf("\n");
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (line.startsWith(":")) {
|
|
114
|
-
newlineIndex = buffer.indexOf("\n");
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
if (line.startsWith("event:")) {
|
|
118
|
-
eventType = line.slice(6).trim();
|
|
119
|
-
}
|
|
120
|
-
else if (line.startsWith("data:")) {
|
|
121
|
-
dataLines.push(line.slice(5).trimStart());
|
|
122
|
-
}
|
|
123
|
-
newlineIndex = buffer.indexOf("\n");
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
// Stream ended or aborted
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
function processEvent(dispatch, eventType, data) {
|
|
132
|
-
try {
|
|
133
|
-
if (eventType === "codex") {
|
|
134
|
-
const parsed = JSON.parse(data);
|
|
135
|
-
dispatch({ type: "codex-notification", method: parsed.method, params: parsed.params });
|
|
136
|
-
}
|
|
137
|
-
// Feed events are handled by the main watch stream
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
// Ignore parse errors
|
|
141
|
-
}
|
|
142
|
-
}
|
|
@@ -5,127 +5,36 @@ export function useWatchStream(options) {
|
|
|
5
5
|
useEffect(() => {
|
|
6
6
|
if (options.active === false)
|
|
7
7
|
return;
|
|
8
|
-
|
|
9
|
-
let reconnectTimeout;
|
|
10
|
-
let attempt = 0;
|
|
8
|
+
const abortController = new AbortController();
|
|
11
9
|
const fetchIssueSnapshot = async () => {
|
|
12
10
|
const { baseUrl, bearerToken, dispatch } = optionsRef.current;
|
|
13
11
|
const headers = { accept: "application/json" };
|
|
14
12
|
if (bearerToken) {
|
|
15
13
|
headers.authorization = `Bearer ${bearerToken}`;
|
|
16
14
|
}
|
|
17
|
-
const response = await fetch(new URL("/api/
|
|
15
|
+
const response = await fetch(new URL("/api/issues", baseUrl), { headers, signal: abortController.signal });
|
|
18
16
|
if (!response.ok) {
|
|
19
17
|
throw new Error(`Issue snapshot failed: ${response.status}`);
|
|
20
18
|
}
|
|
21
19
|
const payload = await response.json();
|
|
20
|
+
dispatch({ type: "connected" });
|
|
22
21
|
dispatch({ type: "issues-snapshot", issues: Array.isArray(payload.issues) ? payload.issues : [], receivedAt: Date.now() });
|
|
23
22
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const url = new URL("/api/watch", baseUrl);
|
|
28
|
-
if (issueFilter) {
|
|
29
|
-
url.searchParams.set("issue", issueFilter);
|
|
23
|
+
void fetchIssueSnapshot().catch(() => {
|
|
24
|
+
if (!abortController.signal.aborted) {
|
|
25
|
+
optionsRef.current.dispatch({ type: "disconnected" });
|
|
30
26
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.then(async (response) => {
|
|
37
|
-
if (!response.ok || !response.body) {
|
|
38
|
-
throw new Error(`Watch stream failed: ${response.status}`);
|
|
39
|
-
}
|
|
40
|
-
dispatch({ type: "connected" });
|
|
41
|
-
attempt = 0;
|
|
42
|
-
try {
|
|
43
|
-
await fetchIssueSnapshot();
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
// Keep the stream alive even if the snapshot endpoint temporarily fails.
|
|
47
|
-
}
|
|
48
|
-
const reader = response.body.getReader();
|
|
49
|
-
const decoder = new TextDecoder();
|
|
50
|
-
let buffer = "";
|
|
51
|
-
let eventType = "";
|
|
52
|
-
let dataLines = [];
|
|
53
|
-
while (true) {
|
|
54
|
-
const { done, value } = await reader.read();
|
|
55
|
-
if (done)
|
|
56
|
-
break;
|
|
57
|
-
buffer += decoder.decode(value, { stream: true });
|
|
58
|
-
let newlineIndex = buffer.indexOf("\n");
|
|
59
|
-
while (newlineIndex !== -1) {
|
|
60
|
-
const rawLine = buffer.slice(0, newlineIndex);
|
|
61
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
62
|
-
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
63
|
-
if (!line) {
|
|
64
|
-
if (dataLines.length > 0) {
|
|
65
|
-
processEvent(dispatch, eventType, dataLines.join("\n"));
|
|
66
|
-
dataLines = [];
|
|
67
|
-
eventType = "";
|
|
68
|
-
}
|
|
69
|
-
newlineIndex = buffer.indexOf("\n");
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (line.startsWith(":")) {
|
|
73
|
-
if (line.includes("keepalive")) {
|
|
74
|
-
dispatch({ type: "stream-heartbeat", receivedAt: Date.now() });
|
|
75
|
-
}
|
|
76
|
-
newlineIndex = buffer.indexOf("\n");
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (line.startsWith("event:")) {
|
|
80
|
-
eventType = line.slice(6).trim();
|
|
81
|
-
}
|
|
82
|
-
else if (line.startsWith("data:")) {
|
|
83
|
-
dataLines.push(line.slice(5).trimStart());
|
|
84
|
-
}
|
|
85
|
-
newlineIndex = buffer.indexOf("\n");
|
|
86
|
-
}
|
|
27
|
+
});
|
|
28
|
+
const snapshotInterval = setInterval(() => {
|
|
29
|
+
void fetchIssueSnapshot().catch(() => {
|
|
30
|
+
if (!abortController.signal.aborted) {
|
|
31
|
+
optionsRef.current.dispatch({ type: "disconnected" });
|
|
87
32
|
}
|
|
88
|
-
})
|
|
89
|
-
.catch((error) => {
|
|
90
|
-
if (abortController.signal.aborted)
|
|
91
|
-
return;
|
|
92
|
-
const _msg = error instanceof Error ? error.message : String(error);
|
|
93
|
-
})
|
|
94
|
-
.finally(() => {
|
|
95
|
-
if (abortController.signal.aborted)
|
|
96
|
-
return;
|
|
97
|
-
dispatch({ type: "disconnected" });
|
|
98
|
-
attempt = Math.min(attempt + 1, 5);
|
|
99
|
-
const delay = Math.min(2000 * Math.pow(2, attempt), 30000);
|
|
100
|
-
reconnectTimeout = setTimeout(connect, delay);
|
|
101
33
|
});
|
|
102
|
-
};
|
|
103
|
-
connect();
|
|
104
|
-
void fetchIssueSnapshot().catch(() => undefined);
|
|
105
|
-
const snapshotInterval = setInterval(() => {
|
|
106
|
-
void fetchIssueSnapshot().catch(() => undefined);
|
|
107
34
|
}, 5000);
|
|
108
35
|
return () => {
|
|
109
36
|
abortController.abort();
|
|
110
|
-
if (reconnectTimeout !== undefined) {
|
|
111
|
-
clearTimeout(reconnectTimeout);
|
|
112
|
-
}
|
|
113
37
|
clearInterval(snapshotInterval);
|
|
114
38
|
};
|
|
115
39
|
}, [options.active]);
|
|
116
40
|
}
|
|
117
|
-
function processEvent(dispatch, eventType, data) {
|
|
118
|
-
try {
|
|
119
|
-
if (eventType === "issues") {
|
|
120
|
-
const issues = JSON.parse(data);
|
|
121
|
-
dispatch({ type: "issues-snapshot", issues, receivedAt: Date.now() });
|
|
122
|
-
}
|
|
123
|
-
else if (eventType === "feed") {
|
|
124
|
-
const event = JSON.parse(data);
|
|
125
|
-
dispatch({ type: "feed-event", event, receivedAt: Date.now() });
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
// Ignore parse errors from malformed events
|
|
130
|
-
}
|
|
131
|
-
}
|
|
@@ -31,6 +31,9 @@ export const initialWatchState = {
|
|
|
31
31
|
feedEvents: [],
|
|
32
32
|
};
|
|
33
33
|
const TERMINAL_FACTORY_STATES = new Set(["done", "failed"]);
|
|
34
|
+
function effectiveSessionState(issue) {
|
|
35
|
+
return issue.sessionState ?? (TERMINAL_FACTORY_STATES.has(issue.factoryState) ? issue.factoryState : undefined);
|
|
36
|
+
}
|
|
34
37
|
export function filterIssues(issues, filter) {
|
|
35
38
|
switch (filter) {
|
|
36
39
|
case "all":
|
|
@@ -38,7 +41,10 @@ export function filterIssues(issues, filter) {
|
|
|
38
41
|
case "active":
|
|
39
42
|
return issues.filter((i) => i.activeRunType !== undefined);
|
|
40
43
|
case "non-done":
|
|
41
|
-
return issues.filter((i) =>
|
|
44
|
+
return issues.filter((i) => {
|
|
45
|
+
const sessionState = effectiveSessionState(i);
|
|
46
|
+
return sessionState !== "done" && sessionState !== "failed" && !TERMINAL_FACTORY_STATES.has(i.factoryState);
|
|
47
|
+
});
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
const DONE_STATES = new Set(["done"]);
|
|
@@ -50,15 +56,18 @@ export function computeAggregates(issues) {
|
|
|
50
56
|
let done = 0;
|
|
51
57
|
let failed = 0;
|
|
52
58
|
for (const issue of issues) {
|
|
59
|
+
const sessionState = effectiveSessionState(issue);
|
|
60
|
+
const isDone = sessionState === "done" || DONE_STATES.has(issue.factoryState);
|
|
61
|
+
const isFailed = sessionState === "failed" || FAILED_STATES.has(issue.factoryState);
|
|
53
62
|
if (issue.activeRunType)
|
|
54
63
|
active++;
|
|
55
64
|
if (!issue.activeRunType && issue.blockedByCount > 0)
|
|
56
65
|
blocked++;
|
|
57
|
-
if (!issue.activeRunType && issue.readyForExecution)
|
|
66
|
+
if (!issue.activeRunType && issue.readyForExecution && !isDone && !isFailed)
|
|
58
67
|
ready++;
|
|
59
|
-
if (
|
|
68
|
+
if (isDone)
|
|
60
69
|
done++;
|
|
61
|
-
if (
|
|
70
|
+
if (isFailed)
|
|
62
71
|
failed++;
|
|
63
72
|
}
|
|
64
73
|
return { active, blocked, ready, done, failed, total: issues.length };
|
|
@@ -119,7 +128,7 @@ export function watchReducer(state, action) {
|
|
|
119
128
|
rawRuns: action.runs,
|
|
120
129
|
rawFeedEvents: action.feedEvents,
|
|
121
130
|
activeRunId: action.activeRunId,
|
|
122
|
-
activeRunStartedAt: activeRun?.startedAt ?? null,
|
|
131
|
+
activeRunStartedAt: action.activeRunStartedAt ?? activeRun?.startedAt ?? null,
|
|
123
132
|
issueContext: action.issueContext,
|
|
124
133
|
};
|
|
125
134
|
}
|
|
@@ -145,57 +154,16 @@ export function watchReducer(state, action) {
|
|
|
145
154
|
}
|
|
146
155
|
// ─── Feed Event → Issue List + Timeline ───────────────────────────
|
|
147
156
|
function applyFeedEvent(state, event, receivedAt) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const index = state.issues.findIndex((issue) => issue.issueKey === event.issueKey);
|
|
152
|
-
if (index === -1) {
|
|
153
|
-
return { ...state, lastServerMessageAt: receivedAt };
|
|
154
|
-
}
|
|
155
|
-
const updated = [...state.issues];
|
|
156
|
-
const issue = { ...updated[index] };
|
|
157
|
-
if (event.kind === "stage" && event.stage) {
|
|
158
|
-
issue.factoryState = event.stage;
|
|
159
|
-
}
|
|
160
|
-
if (event.kind === "stage" && event.status === "starting" && event.stage) {
|
|
161
|
-
issue.activeRunType = event.stage;
|
|
162
|
-
}
|
|
163
|
-
if (event.kind === "turn") {
|
|
164
|
-
if (event.status === "completed" || event.status === "failed") {
|
|
165
|
-
issue.activeRunType = undefined;
|
|
166
|
-
issue.latestRunStatus = event.status;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (event.kind === "github" && event.status) {
|
|
170
|
-
if (event.status === "check_passed" || event.status === "check_failed") {
|
|
171
|
-
issue.prCheckStatus = event.status === "check_passed" ? "passed" : "failed";
|
|
172
|
-
}
|
|
173
|
-
if (event.status === "ci_repair_queued") {
|
|
174
|
-
issue.factoryState = "repairing_ci";
|
|
175
|
-
issue.statusNote = event.detail ?? event.summary;
|
|
176
|
-
}
|
|
177
|
-
if (event.status === "queue_repair_queued") {
|
|
178
|
-
issue.factoryState = "repairing_queue";
|
|
179
|
-
issue.statusNote = event.detail ?? event.summary;
|
|
180
|
-
}
|
|
181
|
-
if (event.status === "repair_deduped" || event.status === "branch_not_advanced") {
|
|
182
|
-
issue.statusNote = event.summary;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if ((event.kind === "turn" || event.kind === "github") && event.status === "branch_not_advanced") {
|
|
186
|
-
issue.statusNote = event.summary;
|
|
187
|
-
}
|
|
188
|
-
issue.updatedAt = event.at;
|
|
189
|
-
updated[index] = issue;
|
|
190
|
-
// Append to timeline and raw feed events if this event matches the active detail issue
|
|
191
|
-
const isActiveDetail = state.view === "detail" && state.activeDetailKey === event.issueKey;
|
|
157
|
+
const isActiveDetail = Boolean(event.issueKey)
|
|
158
|
+
&& state.view === "detail"
|
|
159
|
+
&& state.activeDetailKey === event.issueKey;
|
|
192
160
|
const timeline = isActiveDetail
|
|
193
161
|
? capArray(appendFeedToTimeline(state.timeline, event), MAX_TIMELINE_ENTRIES)
|
|
194
162
|
: state.timeline;
|
|
195
163
|
const rawFeedEvents = isActiveDetail
|
|
196
164
|
? capArray([...state.rawFeedEvents, event], MAX_RAW_FEED_EVENTS)
|
|
197
165
|
: state.rawFeedEvents;
|
|
198
|
-
return { ...state, lastServerMessageAt: receivedAt,
|
|
166
|
+
return { ...state, lastServerMessageAt: receivedAt, timeline, rawFeedEvents };
|
|
199
167
|
}
|
|
200
168
|
// ─── Codex Notification → Timeline + Metadata ─────────────────────
|
|
201
169
|
function applyCodexNotification(state, method, params) {
|