patchrelay 0.68.4 → 0.68.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.
- package/dist/build-info.json +3 -3
- package/dist/codex-app-server.js +46 -9
- package/dist/run-orchestrator.js +1 -1
- package/dist/run-reconciler.js +57 -1
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/codex-app-server.js
CHANGED
|
@@ -286,12 +286,22 @@ export class CodexAppServerClient extends EventEmitter {
|
|
|
286
286
|
},
|
|
287
287
|
});
|
|
288
288
|
});
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
289
|
+
try {
|
|
290
|
+
this.writeMessage({
|
|
291
|
+
jsonrpc: "2.0",
|
|
292
|
+
id,
|
|
293
|
+
method,
|
|
294
|
+
params,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
299
|
+
const pending = this.pending.get(id);
|
|
300
|
+
if (pending) {
|
|
301
|
+
this.pending.delete(id);
|
|
302
|
+
pending.reject(err);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
295
305
|
return promise.catch((error) => {
|
|
296
306
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
297
307
|
this.logger.error({
|
|
@@ -390,10 +400,37 @@ export class CodexAppServerClient extends EventEmitter {
|
|
|
390
400
|
}
|
|
391
401
|
}
|
|
392
402
|
writeMessage(message) {
|
|
393
|
-
|
|
394
|
-
|
|
403
|
+
const child = this.child;
|
|
404
|
+
const stdin = child?.stdin;
|
|
405
|
+
if (!stdin || stdin.destroyed || stdin.writableEnded || !stdin.writable) {
|
|
406
|
+
const error = new Error("Codex app-server stdin is closed");
|
|
407
|
+
this.handleTransportFailure(error);
|
|
408
|
+
throw error;
|
|
395
409
|
}
|
|
396
|
-
|
|
410
|
+
try {
|
|
411
|
+
stdin.write(`${JSON.stringify(message)}\n`, (error) => {
|
|
412
|
+
if (error) {
|
|
413
|
+
this.handleTransportFailure(error instanceof Error ? error : new Error(String(error)));
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
419
|
+
this.handleTransportFailure(err);
|
|
420
|
+
throw err;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
handleTransportFailure(error) {
|
|
424
|
+
const child = this.child;
|
|
425
|
+
this.started = false;
|
|
426
|
+
this.child = undefined;
|
|
427
|
+
this.stdoutBuffer = "";
|
|
428
|
+
this.logger.error({
|
|
429
|
+
error: sanitizeDiagnosticText(error.message),
|
|
430
|
+
pendingRequestCount: this.pending.size,
|
|
431
|
+
}, "Codex app-server transport failed");
|
|
432
|
+
this.rejectAllPending(error);
|
|
433
|
+
child?.kill("SIGTERM");
|
|
397
434
|
}
|
|
398
435
|
drainMessages() {
|
|
399
436
|
while (true) {
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -134,7 +134,7 @@ export class RunOrchestrator {
|
|
|
134
134
|
this.runNotificationHandler = new RunNotificationHandler(config, db, logger, this.linearSync, this.runFinalizer, this.threadPorts.readThreadWithRetry, this.leasePorts.withHeldLease, this.leasePorts.heartbeatLease, this.leasePorts.releaseLease, feed);
|
|
135
135
|
this.runRecovery = new RunRecoveryService(db, logger, this.linearSync, this.leasePorts.withHeldLease, this.leasePorts.getHeldLease, (lease, issue, runType, context, dedupeScope) => this.appendWakeEventWithLease(lease, issue, runType, context, dedupeScope), this.leasePorts.releaseLease, (projectId, issueId) => this.enqueueIssue(projectId, issueId), feed);
|
|
136
136
|
this.interruptedRunRecovery = new InterruptedRunRecovery(db, logger, this.linearSync, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, this.recoveryPorts.failRunAndClear, this.recoveryPorts.restoreIdleWorktree, this.runCompletionPolicy, (projectId, issueId) => this.enqueueIssue(projectId, issueId), feed);
|
|
137
|
-
this.runReconciler = new RunReconciler(db, logger, linearProvider, this.linearSync, this.interruptedRunRecovery, this.runFinalizer, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, this.threadPorts.readThreadWithRetry, this.recoveryPorts.recoverOrEscalate, feed);
|
|
137
|
+
this.runReconciler = new RunReconciler(db, logger, linearProvider, this.linearSync, this.interruptedRunRecovery, this.runFinalizer, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, this.threadPorts.readThreadWithRetry, this.recoveryPorts.recoverOrEscalate, (projectId) => this.config.projects.find((project) => project.id === projectId)?.github?.repoFullName, feed);
|
|
138
138
|
this.runWakePlanner = new RunWakePlanner(db);
|
|
139
139
|
this.idleReconciler = new IdleIssueReconciler(db, config, this.wakeDispatcher, logger, feed);
|
|
140
140
|
this.mainBranchHealthMonitor = new MainBranchHealthMonitor(db, config, linearProvider, this.wakeDispatcher, logger, feed);
|
package/dist/run-reconciler.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getThreadTurns } from "./codex-thread-utils.js";
|
|
|
6
6
|
import { resolveRecoverablePostRunState } from "./interrupted-run-recovery.js";
|
|
7
7
|
import { resolveEffectiveActiveRun } from "./effective-active-run.js";
|
|
8
8
|
import { isThreadMaterializingError } from "./codex-thread-errors.js";
|
|
9
|
+
import { fetchPullRequestSnapshot } from "./reconcile-pr-fetch.js";
|
|
9
10
|
const THREAD_MATERIALIZATION_GRACE_MS = 10 * 60_000;
|
|
10
11
|
function isWithinThreadMaterializationGrace(run, nowMs = Date.now()) {
|
|
11
12
|
const startedAtMs = Date.parse(run.startedAt);
|
|
@@ -24,8 +25,9 @@ export class RunReconciler {
|
|
|
24
25
|
releaseLease;
|
|
25
26
|
readThreadWithRetry;
|
|
26
27
|
recoverOrEscalate;
|
|
28
|
+
resolveRepoFullName;
|
|
27
29
|
feed;
|
|
28
|
-
constructor(db, logger, linearProvider, linearSync, interruptedRunRecovery, runFinalizer, withHeldLease, releaseLease, readThreadWithRetry, recoverOrEscalate, feed) {
|
|
30
|
+
constructor(db, logger, linearProvider, linearSync, interruptedRunRecovery, runFinalizer, withHeldLease, releaseLease, readThreadWithRetry, recoverOrEscalate, resolveRepoFullName = () => undefined, feed) {
|
|
29
31
|
this.db = db;
|
|
30
32
|
this.logger = logger;
|
|
31
33
|
this.linearProvider = linearProvider;
|
|
@@ -36,6 +38,7 @@ export class RunReconciler {
|
|
|
36
38
|
this.releaseLease = releaseLease;
|
|
37
39
|
this.readThreadWithRetry = readThreadWithRetry;
|
|
38
40
|
this.recoverOrEscalate = recoverOrEscalate;
|
|
41
|
+
this.resolveRepoFullName = resolveRepoFullName;
|
|
39
42
|
this.feed = feed;
|
|
40
43
|
}
|
|
41
44
|
async reconcile(params) {
|
|
@@ -76,6 +79,9 @@ export class RunReconciler {
|
|
|
76
79
|
this.releaseLease(run.projectId, run.linearIssueId);
|
|
77
80
|
return;
|
|
78
81
|
}
|
|
82
|
+
if (await this.releaseRunIfPullRequestMerged(run, effectiveIssue)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
79
85
|
if (!run.threadId) {
|
|
80
86
|
if (recoveryLease === "owned") {
|
|
81
87
|
this.logger.debug({ issueKey: effectiveIssue.issueKey, runId: run.id, runType: run.runType }, "Skipping zombie reconciliation for locally-owned launch that has not created a thread yet");
|
|
@@ -171,6 +177,56 @@ export class RunReconciler {
|
|
|
171
177
|
this.releaseLease(run.projectId, run.linearIssueId);
|
|
172
178
|
}
|
|
173
179
|
}
|
|
180
|
+
async releaseRunIfPullRequestMerged(run, issue) {
|
|
181
|
+
if (issue.prNumber === undefined)
|
|
182
|
+
return false;
|
|
183
|
+
if (issue.prState === "merged") {
|
|
184
|
+
this.releaseMergedRun(run, issue, "Cached PR state is merged");
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
const repoFullName = this.resolveRepoFullName(issue.projectId);
|
|
188
|
+
if (!repoFullName)
|
|
189
|
+
return false;
|
|
190
|
+
const snapshot = await fetchPullRequestSnapshot(repoFullName, issue.prNumber);
|
|
191
|
+
if (!snapshot.ok) {
|
|
192
|
+
this.logger.debug({ issueKey: issue.issueKey, prNumber: issue.prNumber, error: snapshot.error.message }, "Could not refresh active-run PR state during reconciliation");
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
if (snapshot.pr.state !== "MERGED")
|
|
196
|
+
return false;
|
|
197
|
+
this.releaseMergedRun(run, issue, "Pull request merged while the active Codex run was still marked running");
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
releaseMergedRun(run, issue, reason) {
|
|
201
|
+
this.withHeldLease(run.projectId, run.linearIssueId, () => {
|
|
202
|
+
this.db.issueSessions.clearPendingIssueSessionEvents(run.projectId, run.linearIssueId);
|
|
203
|
+
this.db.runs.finishRun(run.id, {
|
|
204
|
+
status: "released",
|
|
205
|
+
failureReason: reason,
|
|
206
|
+
});
|
|
207
|
+
this.db.issues.upsertIssue({
|
|
208
|
+
projectId: run.projectId,
|
|
209
|
+
linearIssueId: run.linearIssueId,
|
|
210
|
+
activeRunId: null,
|
|
211
|
+
factoryState: "done",
|
|
212
|
+
prState: "merged",
|
|
213
|
+
pendingRunType: null,
|
|
214
|
+
pendingRunContextJson: null,
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
this.feed?.publish({
|
|
218
|
+
level: "info",
|
|
219
|
+
kind: "stage",
|
|
220
|
+
issueKey: issue.issueKey,
|
|
221
|
+
projectId: run.projectId,
|
|
222
|
+
stage: "done",
|
|
223
|
+
status: "reconciled",
|
|
224
|
+
summary: `Released active ${run.runType} run after PR merge`,
|
|
225
|
+
});
|
|
226
|
+
const doneIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
227
|
+
void this.linearSync.syncSession(doneIssue, { activeRunType: run.runType });
|
|
228
|
+
this.releaseLease(run.projectId, run.linearIssueId);
|
|
229
|
+
}
|
|
174
230
|
async confirmDelegationAuthorityBeforeRelease(run, issue) {
|
|
175
231
|
const installation = this.db.linearInstallations.getLinearInstallationForProject(run.projectId);
|
|
176
232
|
const linear = await this.linearProvider.forProject(run.projectId).catch(() => undefined);
|