patchrelay 0.2.0 → 0.4.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.
@@ -91,6 +91,23 @@ CREATE TABLE IF NOT EXISTS run_leases (
91
91
  FOREIGN KEY(trigger_receipt_id) REFERENCES event_receipts(id) ON DELETE SET NULL
92
92
  );
93
93
 
94
+ CREATE TABLE IF NOT EXISTS issue_sessions (
95
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
96
+ project_id TEXT NOT NULL,
97
+ linear_issue_id TEXT NOT NULL,
98
+ workspace_ownership_id INTEGER NOT NULL,
99
+ run_lease_id INTEGER,
100
+ thread_id TEXT NOT NULL UNIQUE,
101
+ parent_thread_id TEXT,
102
+ source TEXT NOT NULL,
103
+ linked_agent_session_id TEXT,
104
+ created_at TEXT NOT NULL,
105
+ updated_at TEXT NOT NULL,
106
+ last_opened_at TEXT,
107
+ FOREIGN KEY(workspace_ownership_id) REFERENCES workspace_ownership(id) ON DELETE CASCADE,
108
+ FOREIGN KEY(run_lease_id) REFERENCES run_leases(id) ON DELETE SET NULL
109
+ );
110
+
94
111
  CREATE TABLE IF NOT EXISTS run_reports (
95
112
  run_lease_id INTEGER PRIMARY KEY,
96
113
  summary_json TEXT,
@@ -170,6 +187,8 @@ CREATE TABLE IF NOT EXISTS oauth_states (
170
187
  CREATE INDEX IF NOT EXISTS idx_event_receipts_project_issue ON event_receipts(project_id, linear_issue_id);
171
188
  CREATE INDEX IF NOT EXISTS idx_issue_control_ready ON issue_control(desired_stage, active_run_lease_id);
172
189
  CREATE INDEX IF NOT EXISTS idx_issue_projection_issue_key ON issue_projection(issue_key);
190
+ CREATE INDEX IF NOT EXISTS idx_issue_sessions_issue ON issue_sessions(project_id, linear_issue_id, id DESC);
191
+ CREATE INDEX IF NOT EXISTS idx_issue_sessions_last_opened ON issue_sessions(project_id, linear_issue_id, last_opened_at DESC, id DESC);
173
192
  CREATE INDEX IF NOT EXISTS idx_run_leases_active ON run_leases(status, project_id, linear_issue_id);
174
193
  CREATE INDEX IF NOT EXISTS idx_run_leases_thread ON run_leases(thread_id);
175
194
  CREATE INDEX IF NOT EXISTS idx_run_thread_events_run ON run_thread_events(run_lease_id, id);
@@ -0,0 +1,33 @@
1
+ import { isoNow } from "./shared.js";
2
+ export class RunReportStore {
3
+ connection;
4
+ constructor(connection) {
5
+ this.connection = connection;
6
+ }
7
+ saveRunReport(params) {
8
+ const now = isoNow();
9
+ this.connection
10
+ .prepare(`
11
+ INSERT INTO run_reports (run_lease_id, summary_json, report_json, created_at, updated_at)
12
+ VALUES (?, ?, ?, ?, ?)
13
+ ON CONFLICT(run_lease_id) DO UPDATE SET
14
+ summary_json = excluded.summary_json,
15
+ report_json = excluded.report_json,
16
+ updated_at = excluded.updated_at
17
+ `)
18
+ .run(params.runLeaseId, params.summaryJson ?? null, params.reportJson ?? null, now, now);
19
+ }
20
+ getRunReport(runLeaseId) {
21
+ const row = this.connection.prepare("SELECT * FROM run_reports WHERE run_lease_id = ?").get(runLeaseId);
22
+ return row ? mapRunReport(row) : undefined;
23
+ }
24
+ }
25
+ function mapRunReport(row) {
26
+ return {
27
+ runLeaseId: Number(row.run_lease_id),
28
+ ...(row.summary_json === null ? {} : { summaryJson: String(row.summary_json) }),
29
+ ...(row.report_json === null ? {} : { reportJson: String(row.report_json) }),
30
+ createdAt: String(row.created_at),
31
+ updatedAt: String(row.updated_at),
32
+ };
33
+ }
package/dist/db.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { AuthoritativeLedgerStore } from "./db/authoritative-ledger-store.js";
2
+ import { IssueProjectionStore } from "./db/issue-projection-store.js";
3
+ import { IssueWorkflowCoordinator } from "./db/issue-workflow-coordinator.js";
2
4
  import { IssueWorkflowStore } from "./db/issue-workflow-store.js";
3
5
  import { LinearInstallationStore } from "./db/linear-installation-store.js";
4
6
  import { runPatchRelayMigrations } from "./db/migrations.js";
7
+ import { RunReportStore } from "./db/run-report-store.js";
5
8
  import { StageEventStore } from "./db/stage-event-store.js";
6
9
  import { SqliteConnection } from "./db/shared.js";
7
10
  import { WebhookEventStore } from "./db/webhook-event-store.js";
@@ -11,10 +14,14 @@ export class PatchRelayDatabase {
11
14
  eventReceipts;
12
15
  issueControl;
13
16
  workspaceOwnership;
17
+ issueSessions;
14
18
  runLeases;
15
19
  obligations;
16
20
  webhookEvents;
21
+ issueProjections;
17
22
  issueWorkflows;
23
+ workflowCoordinator;
24
+ runReports;
18
25
  stageEvents;
19
26
  linearInstallations;
20
27
  constructor(databasePath, wal) {
@@ -27,10 +34,24 @@ export class PatchRelayDatabase {
27
34
  this.eventReceipts = this.authoritativeLedger;
28
35
  this.issueControl = this.authoritativeLedger;
29
36
  this.workspaceOwnership = this.authoritativeLedger;
37
+ this.issueSessions = this.authoritativeLedger;
30
38
  this.runLeases = this.authoritativeLedger;
31
39
  this.obligations = this.authoritativeLedger;
32
40
  this.webhookEvents = new WebhookEventStore(this.connection);
33
- this.issueWorkflows = new IssueWorkflowStore(this.connection);
41
+ this.issueProjections = new IssueProjectionStore(this.connection);
42
+ this.runReports = new RunReportStore(this.connection);
43
+ this.issueWorkflows = new IssueWorkflowStore({
44
+ authoritativeLedger: this.authoritativeLedger,
45
+ issueProjections: this.issueProjections,
46
+ runReports: this.runReports,
47
+ });
48
+ this.workflowCoordinator = new IssueWorkflowCoordinator({
49
+ connection: this.connection,
50
+ authoritativeLedger: this.authoritativeLedger,
51
+ issueProjections: this.issueProjections,
52
+ issueWorkflows: this.issueWorkflows,
53
+ runReports: this.runReports,
54
+ });
34
55
  this.stageEvents = new StageEventStore(this.connection);
35
56
  this.linearInstallations = new LinearInstallationStore(this.connection);
36
57
  }
package/dist/index.js CHANGED
@@ -42,10 +42,19 @@ async function main() {
42
42
  const service = new PatchRelayService(config, db, codex, linearProvider, logger);
43
43
  await service.start();
44
44
  const app = await buildHttpServer(config, service, logger);
45
- await app.listen({
46
- host: config.server.bind,
47
- port: config.server.port,
48
- });
45
+ try {
46
+ await app.listen({
47
+ host: config.server.bind,
48
+ port: config.server.port,
49
+ });
50
+ }
51
+ catch (error) {
52
+ if (error && typeof error === "object" && "code" in error && error.code === "EADDRINUSE") {
53
+ throw new Error(`Port ${config.server.port} on ${config.server.bind} is already in use. ` +
54
+ `Another patchrelay process may be running. Check with: ss -tlnp | grep ${config.server.port}`, { cause: error });
55
+ }
56
+ throw error;
57
+ }
49
58
  logger.info({
50
59
  bind: config.server.bind,
51
60
  port: config.server.port,
package/dist/install.js CHANGED
@@ -193,11 +193,12 @@ export async function upsertProjectInConfig(options) {
193
193
  const original = await readFile(configPath, "utf8");
194
194
  const parsed = parseConfigObject(original, configPath);
195
195
  const existingProjects = Array.isArray(parsed.projects) ? parsed.projects : [];
196
- const existingIndex = existingProjects.findIndex((project) => String(project.id ?? "") === projectId);
196
+ const existingIndex = existingProjects.findIndex((project) => String(project.id ?? "").toLowerCase() === projectId.toLowerCase());
197
197
  const existingProject = existingIndex >= 0 ? existingProjects[existingIndex] : undefined;
198
+ const resolvedProjectId = existingProject ? String(existingProject.id ?? projectId) : projectId;
198
199
  const nextProject = {
199
200
  ...(existingProject ?? {}),
200
- id: projectId,
201
+ id: resolvedProjectId,
201
202
  repo_path: repoPath,
202
203
  workflows: Array.isArray(existingProject?.workflows) && existingProject.workflows.length > 0
203
204
  ? existingProject.workflows
@@ -293,7 +294,7 @@ export async function upsertProjectInConfig(options) {
293
294
  configPath,
294
295
  status,
295
296
  project: {
296
- id: projectId,
297
+ id: resolvedProjectId,
297
298
  repoPath,
298
299
  issueKeyPrefixes,
299
300
  linearTeamIds,
@@ -20,19 +20,20 @@ export async function exchangeLinearOAuthCode(config, params) {
20
20
  const response = await fetch(DEFAULT_LINEAR_TOKEN_URL, {
21
21
  method: "POST",
22
22
  headers: {
23
- "content-type": "application/json",
23
+ "content-type": "application/x-www-form-urlencoded",
24
24
  },
25
- body: JSON.stringify({
25
+ body: new URLSearchParams({
26
26
  grant_type: "authorization_code",
27
27
  code: params.code,
28
28
  client_id: config.linear.oauth.clientId,
29
29
  client_secret: config.linear.oauth.clientSecret,
30
30
  redirect_uri: params.redirectUri,
31
- }),
31
+ }).toString(),
32
32
  });
33
33
  const payload = (await response.json().catch(() => undefined));
34
34
  if (!response.ok || !payload) {
35
- throw new Error(`Linear OAuth code exchange failed with HTTP ${response.status}`);
35
+ const detail = payload?.error ? `: ${String(payload.error)}` : "";
36
+ throw new Error(`Linear OAuth code exchange failed with HTTP ${response.status}${detail}`);
36
37
  }
37
38
  const accessToken = typeof payload.access_token === "string" ? payload.access_token : undefined;
38
39
  if (!accessToken) {
@@ -53,14 +54,14 @@ export async function refreshLinearOAuthToken(config, refreshToken) {
53
54
  const response = await fetch(DEFAULT_LINEAR_TOKEN_URL, {
54
55
  method: "POST",
55
56
  headers: {
56
- "content-type": "application/json",
57
+ "content-type": "application/x-www-form-urlencoded",
57
58
  },
58
- body: JSON.stringify({
59
+ body: new URLSearchParams({
59
60
  grant_type: "refresh_token",
60
61
  refresh_token: refreshToken,
61
62
  client_id: config.linear.oauth.clientId,
62
63
  client_secret: config.linear.oauth.clientSecret,
63
- }),
64
+ }).toString(),
64
65
  });
65
66
  const payload = (await response.json().catch(() => undefined));
66
67
  if (!response.ok || !payload) {
@@ -114,7 +114,7 @@ export class ServiceStageFinalizer {
114
114
  ...(params.turnId ? { turnId: params.turnId } : {}),
115
115
  nextLifecycleStatus: params.nextLifecycleStatus ?? (issue.desiredStage ? "queued" : "completed"),
116
116
  });
117
- this.stores.issueWorkflows.finishStageRun({
117
+ this.stores.workflowCoordinator.finishStageRun({
118
118
  stageRunId: stageRun.id,
119
119
  status,
120
120
  threadId: params.threadId,
@@ -133,7 +133,7 @@ export class ServiceStageFinalizer {
133
133
  failureReason: message,
134
134
  nextLifecycleStatus: "failed",
135
135
  });
136
- this.stores.issueWorkflows.finishStageRun({
136
+ this.stores.workflowCoordinator.finishStageRun({
137
137
  stageRunId: stageRun.id,
138
138
  status: "failed",
139
139
  threadId,
@@ -45,7 +45,7 @@ export class ServiceStageRunner {
45
45
  return;
46
46
  }
47
47
  const plan = buildStageLaunchPlan(project, issue, desiredStage);
48
- const claim = this.stores.issueWorkflows.claimStageRun({
48
+ const claim = this.stores.workflowCoordinator.claimStageRun({
49
49
  projectId: item.projectId,
50
50
  linearIssueId: item.issueId,
51
51
  stage: desiredStage,
@@ -83,7 +83,7 @@ export class ServiceStageRunner {
83
83
  }, "Failed to launch Codex stage run");
84
84
  throw err;
85
85
  }
86
- this.stores.issueWorkflows.updateStageRunThread({
86
+ this.stores.workflowCoordinator.updateStageRunThread({
87
87
  stageRunId: claim.stageRun.id,
88
88
  threadId: threadLaunch.threadId,
89
89
  ...(threadLaunch.parentThreadId ? { parentThreadId: threadLaunch.parentThreadId } : {}),
@@ -121,7 +121,7 @@ export class ServiceStageRunner {
121
121
  .forProject(project.id)
122
122
  .then((linear) => linear?.getIssue(linearIssueId))
123
123
  .catch(() => undefined);
124
- return this.stores.issueWorkflows.recordDesiredStage({
124
+ return this.stores.workflowCoordinator.recordDesiredStage({
125
125
  projectId: project.id,
126
126
  linearIssueId,
127
127
  ...(liveIssue?.identifier ? { issueKey: liveIssue.identifier } : existing?.issueKey ? { issueKey: existing.issueKey } : {}),
@@ -168,7 +168,7 @@ export class ServiceStageRunner {
168
168
  async markLaunchFailed(project, issue, stageRun, message, threadId) {
169
169
  const failureThreadId = threadId ?? `launch-failed-${stageRun.id}`;
170
170
  this.runAtomically(() => {
171
- this.stores.issueWorkflows.finishStageRun({
171
+ this.stores.workflowCoordinator.finishStageRun({
172
172
  stageRunId: stageRun.id,
173
173
  status: "failed",
174
174
  threadId: failureThreadId,
package/dist/service.js CHANGED
@@ -13,6 +13,7 @@ function createServiceStores(db) {
13
13
  workspaceOwnership: db.workspaceOwnership,
14
14
  runLeases: db.runLeases,
15
15
  obligations: db.obligations,
16
+ workflowCoordinator: db.workflowCoordinator,
16
17
  issueWorkflows: db.issueWorkflows,
17
18
  stageEvents: db.stageEvents,
18
19
  linearInstallations: db.linearInstallations,
@@ -39,21 +39,14 @@ export async function syncFailedStageToLinear(params) {
39
39
  }
40
40
  if (fallbackState) {
41
41
  await linear.setIssueState(params.stageRun.linearIssueId, fallbackState).catch(() => undefined);
42
- params.stores.issueWorkflows.setIssueLifecycleStatus(params.stageRun.projectId, params.stageRun.linearIssueId, "failed");
43
- params.stores.issueWorkflows.upsertTrackedIssue({
42
+ params.stores.workflowCoordinator.upsertTrackedIssue({
44
43
  projectId: params.stageRun.projectId,
45
44
  linearIssueId: params.stageRun.linearIssueId,
46
45
  currentLinearState: fallbackState,
47
46
  statusCommentId: params.issue.statusCommentId ?? null,
47
+ activeAgentSessionId: params.issue.activeAgentSessionId ?? null,
48
48
  lifecycleStatus: "failed",
49
49
  });
50
- params.stores.issueControl.upsertIssueControl({
51
- projectId: params.stageRun.projectId,
52
- linearIssueId: params.stageRun.linearIssueId,
53
- lifecycleStatus: "failed",
54
- ...(params.issue.statusCommentId ? { serviceOwnedCommentId: params.issue.statusCommentId } : {}),
55
- ...(params.issue.activeAgentSessionId ? { activeAgentSessionId: params.issue.activeAgentSessionId } : {}),
56
- });
57
50
  }
58
51
  const result = await linear
59
52
  .upsertIssueComment({
@@ -69,14 +62,7 @@ export async function syncFailedStageToLinear(params) {
69
62
  })
70
63
  .catch(() => undefined);
71
64
  if (result) {
72
- params.stores.issueWorkflows.setIssueStatusComment(params.stageRun.projectId, params.stageRun.linearIssueId, result.id);
73
- params.stores.issueControl.upsertIssueControl({
74
- projectId: params.stageRun.projectId,
75
- linearIssueId: params.stageRun.linearIssueId,
76
- serviceOwnedCommentId: result.id,
77
- lifecycleStatus: "failed",
78
- ...(params.issue.activeAgentSessionId ? { activeAgentSessionId: params.issue.activeAgentSessionId } : {}),
79
- });
65
+ params.stores.workflowCoordinator.setIssueStatusComment(params.stageRun.projectId, params.stageRun.linearIssueId, result.id);
80
66
  }
81
67
  if (params.issue.activeAgentSessionId) {
82
68
  await linear
@@ -26,18 +26,12 @@ export class StageLifecyclePublisher {
26
26
  ...(labels.remove.length > 0 ? { removeNames: labels.remove } : {}),
27
27
  });
28
28
  }
29
- this.stores.issueWorkflows.upsertTrackedIssue({
29
+ this.stores.workflowCoordinator.upsertTrackedIssue({
30
30
  projectId: stageRun.projectId,
31
31
  linearIssueId: stageRun.linearIssueId,
32
32
  currentLinearState: activeState,
33
33
  statusCommentId: issue.statusCommentId ?? null,
34
- lifecycleStatus: "running",
35
- });
36
- this.stores.issueControl.upsertIssueControl({
37
- projectId: stageRun.projectId,
38
- linearIssueId: stageRun.linearIssueId,
39
- ...(issue.statusCommentId ? { serviceOwnedCommentId: issue.statusCommentId } : {}),
40
- ...(issue.activeAgentSessionId ? { activeAgentSessionId: issue.activeAgentSessionId } : {}),
34
+ activeAgentSessionId: issue.activeAgentSessionId ?? null,
41
35
  lifecycleStatus: "running",
42
36
  });
43
37
  }
@@ -62,13 +56,7 @@ export class StageLifecyclePublisher {
62
56
  branchName: workspace.branchName,
63
57
  }),
64
58
  });
65
- this.stores.issueWorkflows.setIssueStatusComment(projectId, issueId, result.id);
66
- this.stores.issueControl.upsertIssueControl({
67
- projectId,
68
- linearIssueId: issueId,
69
- serviceOwnedCommentId: result.id,
70
- lifecycleStatus: issue.lifecycleStatus,
71
- });
59
+ this.stores.workflowCoordinator.setIssueStatusComment(projectId, issueId, result.id);
72
60
  }
73
61
  catch (error) {
74
62
  this.logger.warn({
@@ -133,12 +121,7 @@ export class StageLifecyclePublisher {
133
121
  ...(labels.remove.length > 0 ? { removeNames: labels.remove } : {}),
134
122
  });
135
123
  }
136
- this.stores.issueWorkflows.setIssueLifecycleStatus(stageRun.projectId, stageRun.linearIssueId, "paused");
137
- this.stores.issueControl.upsertIssueControl({
138
- projectId: stageRun.projectId,
139
- linearIssueId: stageRun.linearIssueId,
140
- lifecycleStatus: "paused",
141
- });
124
+ this.stores.workflowCoordinator.setIssueLifecycleStatus(stageRun.projectId, stageRun.linearIssueId, "paused");
142
125
  const finalStageRun = this.stores.issueWorkflows.getStageRun(stageRun.id) ?? stageRun;
143
126
  const result = await linear.upsertIssueComment({
144
127
  issueId: stageRun.linearIssueId,
@@ -149,13 +132,7 @@ export class StageLifecyclePublisher {
149
132
  activeState,
150
133
  }),
151
134
  });
152
- this.stores.issueWorkflows.setIssueStatusComment(stageRun.projectId, stageRun.linearIssueId, result.id);
153
- this.stores.issueControl.upsertIssueControl({
154
- projectId: stageRun.projectId,
155
- linearIssueId: stageRun.linearIssueId,
156
- serviceOwnedCommentId: result.id,
157
- lifecycleStatus: "paused",
158
- });
135
+ this.stores.workflowCoordinator.setIssueStatusComment(stageRun.projectId, stageRun.linearIssueId, result.id);
159
136
  await this.publishAgentCompletion(refreshedIssue, {
160
137
  type: "elicitation",
161
138
  body: `PatchRelay finished the ${stageRun.stage} workflow. Move the issue to its next workflow state or leave a follow-up prompt to continue.`,
@@ -27,19 +27,18 @@ export class WebhookDesiredStageRecorder {
27
27
  const stageAllowed = triggerEventAllowed(project, normalized.triggerEvent);
28
28
  const desiredStage = this.resolveDesiredStage(project, normalized, issue, activeStageRun, delegatedToPatchRelay);
29
29
  const launchInput = this.resolveLaunchInput(normalized.agentSession);
30
- this.persistIssueControlFirst(project.id, normalizedIssue.id, issue, activeStageRun, desiredStage, normalized.agentSession?.id, options?.eventReceiptId);
31
- this.stores.issueWorkflows.recordDesiredStage({
30
+ const refreshedIssue = this.stores.workflowCoordinator.recordDesiredStage({
32
31
  projectId: project.id,
33
32
  linearIssueId: normalizedIssue.id,
34
33
  ...(normalizedIssue.identifier ? { issueKey: normalizedIssue.identifier } : {}),
35
34
  ...(normalizedIssue.title ? { title: normalizedIssue.title } : {}),
36
35
  ...(normalizedIssue.url ? { issueUrl: normalizedIssue.url } : {}),
37
36
  ...(normalizedIssue.stateName ? { currentLinearState: normalizedIssue.stateName } : {}),
37
+ ...(desiredStage ? { desiredStage } : {}),
38
+ ...(options?.eventReceiptId !== undefined ? { desiredReceiptId: options.eventReceiptId } : {}),
39
+ ...(normalized.agentSession?.id ? { activeAgentSessionId: normalized.agentSession.id } : {}),
38
40
  lastWebhookAt: new Date().toISOString(),
39
41
  });
40
- if (normalized.agentSession?.id) {
41
- this.stores.issueWorkflows.setIssueActiveAgentSession(project.id, normalizedIssue.id, normalized.agentSession.id);
42
- }
43
42
  if (launchInput && !activeStageRun && delegatedToPatchRelay && stageAllowed) {
44
43
  this.stores.obligations.enqueueObligation({
45
44
  projectId: project.id,
@@ -51,8 +50,6 @@ export class WebhookDesiredStageRecorder {
51
50
  }),
52
51
  });
53
52
  }
54
- const refreshedIssue = this.stores.issueWorkflows.getTrackedIssue(project.id, normalizedIssue.id);
55
- this.syncIssueControl(project.id, normalizedIssue.id, refreshedIssue, desiredStage, normalized.agentSession?.id, options?.eventReceiptId);
56
53
  return {
57
54
  issue: refreshedIssue ?? issue,
58
55
  activeStageRun,
@@ -119,32 +116,4 @@ export class WebhookDesiredStageRecorder {
119
116
  }
120
117
  return undefined;
121
118
  }
122
- persistIssueControlFirst(projectId, linearIssueId, issue, activeStageRun, desiredStage, activeAgentSessionId, eventReceiptId) {
123
- if (!desiredStage) {
124
- return;
125
- }
126
- const lifecycleStatus = issue?.lifecycleStatus ?? "queued";
127
- this.stores.issueControl.upsertIssueControl({
128
- projectId,
129
- linearIssueId,
130
- desiredStage,
131
- ...(eventReceiptId !== undefined ? { desiredReceiptId: eventReceiptId } : {}),
132
- ...(issue?.statusCommentId ? { serviceOwnedCommentId: issue.statusCommentId } : {}),
133
- ...(activeAgentSessionId ? { activeAgentSessionId } : {}),
134
- lifecycleStatus,
135
- });
136
- }
137
- syncIssueControl(projectId, linearIssueId, issue, desiredStage, activeAgentSessionId, eventReceiptId) {
138
- if (!issue) {
139
- return;
140
- }
141
- this.stores.issueControl.upsertIssueControl({
142
- projectId,
143
- linearIssueId,
144
- ...(desiredStage ? { desiredStage } : {}),
145
- ...(eventReceiptId !== undefined && desiredStage ? { desiredReceiptId: eventReceiptId } : {}),
146
- ...(activeAgentSessionId ? { activeAgentSessionId } : {}),
147
- lifecycleStatus: issue.lifecycleStatus,
148
- });
149
- }
150
119
  }
@@ -6,6 +6,8 @@ Unit=patchrelay-reload.service
6
6
  PathChanged=/home/your-user/.config/patchrelay/runtime.env
7
7
  PathChanged=/home/your-user/.config/patchrelay/service.env
8
8
  PathChanged=/home/your-user/.config/patchrelay/patchrelay.json
9
+ TriggerLimitIntervalSec=5
10
+ TriggerLimitBurst=1
9
11
 
10
12
  [Install]
11
13
  WantedBy=default.target
@@ -2,6 +2,8 @@
2
2
  Description=PatchRelay (systemd user service)
3
3
  After=network-online.target
4
4
  Wants=network-online.target
5
+ StartLimitIntervalSec=60
6
+ StartLimitBurst=8
5
7
 
6
8
  [Service]
7
9
  Type=simple
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {