patchrelay 0.74.7 → 0.75.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,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.74.7",
4
- "commit": "53cd46e337f8",
5
- "builtAt": "2026-05-29T11:49:02.626Z"
3
+ "version": "0.75.0",
4
+ "commit": "ad9b09da1d53",
5
+ "builtAt": "2026-05-29T13:21:05.647Z"
6
6
  }
@@ -1,4 +1,7 @@
1
1
  import { useEffect, useRef } from "react";
2
+ const DETAIL_REHYDRATE_INTERVAL_MS = 3000;
3
+ const FEED_REHYDRATE_LIMIT = 100;
4
+ const MAX_CACHED_FEED_EVENTS = 300;
2
5
  export function useDetailStream(options) {
3
6
  const optionsRef = useRef(options);
4
7
  optionsRef.current = options;
@@ -14,10 +17,24 @@ export function useDetailStream(options) {
14
17
  if (bearerToken) {
15
18
  headers.authorization = `Bearer ${bearerToken}`;
16
19
  }
17
- void rehydrate(baseUrl, issueKey, headers, abortController.signal, dispatch);
20
+ const feedState = {
21
+ lastFeedEventId: undefined,
22
+ feedEvents: [],
23
+ };
24
+ let inFlight = false;
25
+ const runRehydrate = () => {
26
+ if (inFlight)
27
+ return;
28
+ inFlight = true;
29
+ void rehydrate(baseUrl, issueKey, headers, abortController.signal, dispatch, feedState)
30
+ .finally(() => {
31
+ inFlight = false;
32
+ });
33
+ };
34
+ runRehydrate();
18
35
  const intervalId = setInterval(() => {
19
- void rehydrate(baseUrl, issueKey, headers, abortController.signal, dispatch);
20
- }, 3000);
36
+ runRehydrate();
37
+ }, DETAIL_REHYDRATE_INTERVAL_MS);
21
38
  return () => {
22
39
  clearInterval(intervalId);
23
40
  abortController.abort();
@@ -25,17 +42,22 @@ export function useDetailStream(options) {
25
42
  }, [options.issueKey, options.active]);
26
43
  }
27
44
  // ─── Rehydration ──────────────────────────────────────────────────
28
- async function rehydrate(baseUrl, issueKey, headers, signal, dispatch) {
45
+ async function rehydrate(baseUrl, issueKey, headers, signal, dispatch, feedState) {
29
46
  try {
30
47
  const url = new URL(`/api/issues/${encodeURIComponent(issueKey)}`, baseUrl);
31
- const response = await fetch(url, { headers: { ...headers, accept: "application/json" }, signal });
48
+ const feedUrl = buildFeedUrl(baseUrl, issueKey, feedState.lastFeedEventId);
49
+ const [response, newFeedEvents] = await Promise.all([
50
+ fetch(url, { headers: { ...headers, accept: "application/json" }, signal }),
51
+ fetchFeedEvents(feedUrl, headers, signal),
52
+ ]);
32
53
  if (!response.ok)
33
54
  return;
55
+ updateFeedState(feedState, newFeedEvents);
34
56
  const data = await response.json();
35
57
  dispatch({
36
58
  type: "timeline-rehydrate",
37
59
  runs: Array.isArray(data.runs) ? data.runs : [],
38
- feedEvents: [],
60
+ feedEvents: feedState.feedEvents,
39
61
  liveThread: data.liveThread ?? null,
40
62
  activeRunId: data.activeRun?.id ?? null,
41
63
  activeRunStartedAt: data.activeRun?.startedAt ?? null,
@@ -46,3 +68,39 @@ async function rehydrate(baseUrl, issueKey, headers, signal, dispatch) {
46
68
  // Rehydration is best-effort
47
69
  }
48
70
  }
71
+ function buildFeedUrl(baseUrl, issueKey, afterId) {
72
+ const feedUrl = new URL(`/api/issues/${encodeURIComponent(issueKey)}/feed`, baseUrl);
73
+ feedUrl.searchParams.set("limit", String(FEED_REHYDRATE_LIMIT));
74
+ if (afterId !== undefined) {
75
+ feedUrl.searchParams.set("afterId", String(afterId));
76
+ }
77
+ return feedUrl;
78
+ }
79
+ async function fetchFeedEvents(url, headers, signal) {
80
+ try {
81
+ const response = await fetch(url, { headers: { ...headers, accept: "application/json" }, signal });
82
+ if (!response.ok)
83
+ return [];
84
+ const data = await response.json();
85
+ return Array.isArray(data.events) ? data.events : [];
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ }
91
+ function updateFeedState(feedState, newEvents) {
92
+ if (newEvents.length === 0)
93
+ return;
94
+ const byId = new Map();
95
+ for (const event of feedState.feedEvents) {
96
+ byId.set(event.id, event);
97
+ }
98
+ for (const event of newEvents) {
99
+ byId.set(event.id, event);
100
+ }
101
+ const feedEvents = [...byId.values()]
102
+ .sort((left, right) => left.id - right.id)
103
+ .slice(-MAX_CACHED_FEED_EVENTS);
104
+ feedState.feedEvents = feedEvents;
105
+ feedState.lastFeedEventId = feedEvents.at(-1)?.id;
106
+ }
@@ -1,7 +1,8 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  const CODEX_STATUS_TIMEOUT_MS = 15_000;
3
3
  function stripAnsiCodes(value) {
4
- return value.replace(/\u001b\[[0-9;]*m/g, "");
4
+ const escape = String.fromCharCode(27);
5
+ return value.replace(new RegExp(`${escape}\\[[0-9;]*m`, "g"), "");
5
6
  }
6
7
  function parseAccountLine(output) {
7
8
  const lines = output.split(/\r?\n/);
package/dist/http.js CHANGED
@@ -264,6 +264,19 @@ export async function buildHttpServer(config, service, logger) {
264
264
  }
265
265
  return reply.send({ ok: true, ...result });
266
266
  });
267
+ app.get("/api/issues/:issueKey/feed", async (request, reply) => {
268
+ const issueKey = request.params.issueKey;
269
+ const afterId = getPositiveIntegerQueryParam(request, "afterId");
270
+ const requestedLimit = getPositiveIntegerQueryParam(request, "limit");
271
+ const result = service.listIssueFeedEvents(issueKey, {
272
+ ...(afterId !== undefined ? { afterId } : {}),
273
+ limit: requestedLimit ? Math.min(requestedLimit, 100) : 100,
274
+ });
275
+ if (!result) {
276
+ return reply.code(404).send({ ok: false, reason: "issue_not_found" });
277
+ }
278
+ return reply.send({ ok: true, events: result.events });
279
+ });
267
280
  app.get("/api/issues/:issueKey/live", async (request, reply) => {
268
281
  const issueKey = request.params.issueKey;
269
282
  const result = await service.getActiveRunStatus(issueKey);
package/dist/index.js CHANGED
@@ -1,7 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import { dirname } from "node:path";
3
3
  import { runCli } from "./cli/index.js";
4
+ function suppressNodeSqliteExperimentalWarning() {
5
+ const originalEmitWarning = process.emitWarning;
6
+ process.emitWarning = function emitWarningWithoutNodeSqliteNoise(warning, optionsOrType, codeOrCtor, ctor) {
7
+ const message = warning instanceof Error ? warning.message : warning;
8
+ const type = typeof optionsOrType === "string" ? optionsOrType : optionsOrType?.type;
9
+ if (type === "ExperimentalWarning" && message.includes("SQLite is an experimental feature")) {
10
+ return;
11
+ }
12
+ return originalEmitWarning.call(process, warning, optionsOrType, codeOrCtor, ctor);
13
+ };
14
+ }
4
15
  async function main() {
16
+ suppressNodeSqliteExperimentalWarning();
5
17
  const cliExitCode = await runCli(process.argv.slice(2));
6
18
  if (cliExitCode !== -1) {
7
19
  process.exitCode = cliExitCode;
package/dist/preflight.js CHANGED
@@ -117,6 +117,9 @@ async function checkLinearApi(graphqlUrl) {
117
117
  if (response.ok) {
118
118
  return pass("linear_api", `Linear GraphQL API is reachable at ${graphqlUrl}`);
119
119
  }
120
+ if (response.status === 401 || response.status === 403) {
121
+ return pass("linear_api", `Linear GraphQL API is reachable at ${graphqlUrl} (authentication required)`);
122
+ }
120
123
  return warn("linear_api", `Linear GraphQL API returned ${response.status} — may be unreachable or rate-limited`);
121
124
  }
122
125
  catch (error) {
package/dist/service.js CHANGED
@@ -338,6 +338,22 @@ export class PatchRelayService {
338
338
  async getIssueOverview(issueKey) {
339
339
  return await this.queryService.getIssueOverview(issueKey);
340
340
  }
341
+ listIssueFeedEvents(issueKey, options) {
342
+ const session = this.db.issueSessions.getIssueSessionByKey(issueKey);
343
+ const issue = this.db.issues.getIssueByKey(issueKey);
344
+ const projectId = session?.projectId ?? issue?.projectId;
345
+ const resolvedIssueKey = session?.issueKey ?? issue?.issueKey ?? issueKey;
346
+ if (!projectId)
347
+ return undefined;
348
+ return {
349
+ events: this.db.operatorFeed.list({
350
+ issueKey: resolvedIssueKey,
351
+ projectId,
352
+ ...(options?.afterId !== undefined ? { afterId: options.afterId } : {}),
353
+ limit: Math.min(options?.limit ?? 100, 100),
354
+ }),
355
+ };
356
+ }
341
357
  async getActiveRunStatus(issueKey) {
342
358
  return await this.orchestrator.getActiveRunStatus(issueKey);
343
359
  }
package/dist/telemetry.js CHANGED
@@ -78,8 +78,24 @@ export class OperatorFeedTelemetrySink {
78
78
  };
79
79
  }
80
80
  return undefined;
81
+ case "health.invariant":
82
+ return {
83
+ level: event.status === "observed" ? "warn" : "info",
84
+ kind: "workflow",
85
+ ...(event.issueKey ? { issueKey: event.issueKey } : {}),
86
+ ...(event.projectId ? { projectId: event.projectId } : {}),
87
+ ...(event.runType ? { stage: event.runType } : {}),
88
+ status: `health_${event.status}`,
89
+ summary: event.status === "observed"
90
+ ? `Health warning: ${formatInvariant(event.invariant)}`
91
+ : `Health repaired: ${formatInvariant(event.invariant)}`,
92
+ ...(event.detail ? { detail: event.detail } : {}),
93
+ };
81
94
  default:
82
95
  return undefined;
83
96
  }
84
97
  }
85
98
  }
99
+ function formatInvariant(invariant) {
100
+ return invariant.replaceAll("_", " ");
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.74.7",
3
+ "version": "0.75.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {