patchrelay 0.12.4 → 0.12.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.12.4",
4
- "commit": "dea4ce43848b",
5
- "builtAt": "2026-03-24T18:27:01.637Z"
3
+ "version": "0.12.5",
4
+ "commit": "7de41a6b5840",
5
+ "builtAt": "2026-03-24T20:31:57.125Z"
6
6
  }
package/dist/db.js CHANGED
@@ -260,7 +260,7 @@ export class PatchRelayDatabase {
260
260
  }
261
261
  listRunningRuns() {
262
262
  const rows = this.connection
263
- .prepare("SELECT * FROM runs WHERE status = 'running'")
263
+ .prepare("SELECT * FROM runs WHERE status IN ('running', 'queued')")
264
264
  .all();
265
265
  return rows.map(mapRunRow);
266
266
  }
@@ -328,24 +328,49 @@ export class RunOrchestrator {
328
328
  }
329
329
  }
330
330
  async reconcileRun(run) {
331
- if (!run.threadId) {
332
- this.failRunAndClear(run, "Run has no thread ID during reconciliation");
333
- return;
334
- }
335
331
  const issue = this.db.getIssue(run.projectId, run.linearIssueId);
336
332
  if (!issue)
337
333
  return;
338
- // Read Codex state
334
+ // Zombie run: claimed in DB but Codex never started (no thread).
335
+ // This happens when the service crashes between claiming the run
336
+ // and starting the Codex turn. Re-enqueue instead of failing.
337
+ if (!run.threadId) {
338
+ this.logger.warn({ issueKey: issue.issueKey, runId: run.id, runType: run.runType }, "Zombie run detected (no thread) — clearing and re-enqueueing");
339
+ this.db.transaction(() => {
340
+ this.db.finishRun(run.id, { status: "failed", failureReason: "Zombie: never started (no thread after restart)" });
341
+ this.db.upsertIssue({
342
+ projectId: run.projectId,
343
+ linearIssueId: run.linearIssueId,
344
+ activeRunId: null,
345
+ pendingRunType: run.runType,
346
+ pendingRunContextJson: null,
347
+ });
348
+ });
349
+ this.enqueueIssue(run.projectId, run.linearIssueId);
350
+ return;
351
+ }
352
+ // Read Codex state — thread may not exist after app-server restart.
339
353
  let thread;
340
354
  try {
341
355
  thread = await this.readThreadWithRetry(run.threadId);
342
356
  }
343
357
  catch {
344
- this.failRunAndClear(run, "Codex thread not found during reconciliation");
358
+ this.logger.warn({ issueKey: issue.issueKey, runId: run.id, runType: run.runType, threadId: run.threadId }, "Stale thread during reconciliation clearing and re-enqueueing");
359
+ this.db.transaction(() => {
360
+ this.db.finishRun(run.id, { status: "failed", failureReason: "Stale thread after restart" });
361
+ this.db.upsertIssue({
362
+ projectId: run.projectId,
363
+ linearIssueId: run.linearIssueId,
364
+ activeRunId: null,
365
+ pendingRunType: run.runType,
366
+ pendingRunContextJson: null,
367
+ });
368
+ });
369
+ this.enqueueIssue(run.projectId, run.linearIssueId);
345
370
  return;
346
371
  }
347
- // Check Linear state
348
- const linear = await this.linearProvider.forProject(run.projectId);
372
+ // Check Linear state (non-fatal — token refresh may fail)
373
+ const linear = await this.linearProvider.forProject(run.projectId).catch(() => undefined);
349
374
  if (linear) {
350
375
  const linearIssue = await linear.getIssue(run.linearIssueId).catch(() => undefined);
351
376
  if (linearIssue) {
@@ -429,10 +454,10 @@ export class RunOrchestrator {
429
454
  async emitLinearActivity(issue, type, body, options) {
430
455
  if (!issue.agentSessionId)
431
456
  return;
432
- const linear = await this.linearProvider.forProject(issue.projectId);
433
- if (!linear)
434
- return;
435
457
  try {
458
+ const linear = await this.linearProvider.forProject(issue.projectId);
459
+ if (!linear)
460
+ return;
436
461
  await linear.createAgentActivity({
437
462
  agentSessionId: issue.agentSessionId,
438
463
  content: { type, body },
@@ -446,10 +471,10 @@ export class RunOrchestrator {
446
471
  async updateLinearPlan(issue, plan) {
447
472
  if (!issue.agentSessionId)
448
473
  return;
449
- const linear = await this.linearProvider.forProject(issue.projectId);
450
- if (!linear?.updateAgentSession)
451
- return;
452
474
  try {
475
+ const linear = await this.linearProvider.forProject(issue.projectId);
476
+ if (!linear?.updateAgentSession)
477
+ return;
453
478
  await linear.updateAgentSession({ agentSessionId: issue.agentSessionId, plan });
454
479
  }
455
480
  catch (error) {
package/dist/service.js CHANGED
@@ -77,6 +77,20 @@ export class PatchRelayService {
77
77
  });
78
78
  }
79
79
  async start() {
80
+ // Verify Linear connectivity for all configured projects before starting.
81
+ // Fail fast on auth errors rather than crashing mid-run.
82
+ for (const project of this.config.projects) {
83
+ try {
84
+ const client = await this.linearProvider.forProject(project.id);
85
+ if (!client) {
86
+ this.logger.warn({ projectId: project.id }, "No Linear installation linked — project will not receive agent session events");
87
+ }
88
+ }
89
+ catch (error) {
90
+ const msg = error instanceof Error ? error.message : String(error);
91
+ throw new Error(`Linear auth failed for project ${project.id}: ${msg}. Re-run "patchrelay connect" to refresh the token.`, { cause: error });
92
+ }
93
+ }
80
94
  if (this.githubAppTokenManager) {
81
95
  await ensureGhWrapper(this.logger);
82
96
  await this.githubAppTokenManager.start();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.12.4",
3
+ "version": "0.12.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {