patchrelay 0.68.4 → 0.68.6
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/db/issue-store.js +22 -4
- package/dist/db.js +3 -0
- package/dist/issue-class.js +26 -0
- package/dist/orchestration-parent-wake.js +1 -1
- package/dist/run-orchestrator.js +3 -3
- package/dist/run-reconciler.js +57 -1
- package/dist/webhooks/comment-wake-handler.js +1 -1
- package/dist/webhooks/desired-stage-recorder.js +1 -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/db/issue-store.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { buildInsertBindings, buildUpdateAssignments } from "./issue-upsert-columns.js";
|
|
2
2
|
import { isoNow } from "./shared.js";
|
|
3
|
+
const CANCELED_OR_DUPLICATE_CHILD_PREDICATE = `
|
|
4
|
+
LOWER(TRIM(COALESCE(child.current_linear_state_type, ''))) NOT IN ('canceled', 'cancelled')
|
|
5
|
+
AND LOWER(TRIM(COALESCE(child.current_linear_state, ''))) NOT IN ('duplicate', 'canceled', 'cancelled')
|
|
6
|
+
`;
|
|
7
|
+
const OPEN_CHILD_PREDICATE = `
|
|
8
|
+
LOWER(TRIM(COALESCE(child.current_linear_state_type, ''))) NOT IN ('completed', 'canceled', 'cancelled')
|
|
9
|
+
AND LOWER(TRIM(COALESCE(child.current_linear_state, ''))) NOT IN ('done', 'completed', 'duplicate', 'canceled', 'cancelled')
|
|
10
|
+
`;
|
|
3
11
|
export class IssueStore {
|
|
4
12
|
connection;
|
|
5
13
|
syncIssueSessionFromIssue;
|
|
@@ -292,6 +300,19 @@ export class IssueStore {
|
|
|
292
300
|
`).all(projectId, parentLinearIssueId);
|
|
293
301
|
return rows.map(mapIssueRow);
|
|
294
302
|
}
|
|
303
|
+
listCanonicalChildIssues(projectId, parentLinearIssueId) {
|
|
304
|
+
const rows = this.connection.prepare(`
|
|
305
|
+
SELECT child.*
|
|
306
|
+
FROM issue_children edges
|
|
307
|
+
JOIN issues child
|
|
308
|
+
ON child.project_id = edges.project_id
|
|
309
|
+
AND child.linear_issue_id = edges.child_linear_issue_id
|
|
310
|
+
WHERE edges.project_id = ? AND edges.parent_linear_issue_id = ?
|
|
311
|
+
AND ${CANCELED_OR_DUPLICATE_CHILD_PREDICATE}
|
|
312
|
+
ORDER BY COALESCE(child.issue_key, child.linear_issue_id) ASC
|
|
313
|
+
`).all(projectId, parentLinearIssueId);
|
|
314
|
+
return rows.map(mapIssueRow);
|
|
315
|
+
}
|
|
295
316
|
countOpenChildIssues(projectId, parentLinearIssueId) {
|
|
296
317
|
const row = this.connection.prepare(`
|
|
297
318
|
SELECT COUNT(*) AS count
|
|
@@ -302,10 +323,7 @@ export class IssueStore {
|
|
|
302
323
|
WHERE edges.project_id = ? AND edges.parent_linear_issue_id = ?
|
|
303
324
|
AND (
|
|
304
325
|
child.linear_issue_id IS NULL
|
|
305
|
-
OR (
|
|
306
|
-
COALESCE(child.current_linear_state_type, '') NOT IN ('completed', 'canceled')
|
|
307
|
-
AND LOWER(TRIM(COALESCE(child.current_linear_state, ''))) NOT IN ('done', 'duplicate', 'canceled')
|
|
308
|
-
)
|
|
326
|
+
OR (${OPEN_CHILD_PREDICATE})
|
|
309
327
|
)
|
|
310
328
|
`).get(projectId, parentLinearIssueId);
|
|
311
329
|
return Number(row?.count ?? 0);
|
package/dist/db.js
CHANGED
|
@@ -163,6 +163,9 @@ export class PatchRelayDatabase {
|
|
|
163
163
|
listChildIssues(projectId, parentLinearIssueId) {
|
|
164
164
|
return this.issues.listChildIssues(projectId, parentLinearIssueId);
|
|
165
165
|
}
|
|
166
|
+
listCanonicalChildIssues(projectId, parentLinearIssueId) {
|
|
167
|
+
return this.issues.listCanonicalChildIssues(projectId, parentLinearIssueId);
|
|
168
|
+
}
|
|
166
169
|
countOpenChildIssues(projectId, parentLinearIssueId) {
|
|
167
170
|
return this.issues.countOpenChildIssues(projectId, parentLinearIssueId);
|
|
168
171
|
}
|
package/dist/issue-class.js
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
function hasExplicitNoCodePlanningSplitIntent(issue) {
|
|
2
|
+
const text = [issue.title, issue.description].filter(Boolean).join("\n").toLowerCase();
|
|
3
|
+
if (!text.trim())
|
|
4
|
+
return false;
|
|
5
|
+
const noCodePlanning = [
|
|
6
|
+
/\bno code\b/,
|
|
7
|
+
/\bcode (?:is )?not (?:needed|required|part of this)\b/,
|
|
8
|
+
/\bplanning only\b/,
|
|
9
|
+
/\banalysis only\b/,
|
|
10
|
+
/код[^\n.]{0,80}не делаем/,
|
|
11
|
+
/без (?:изменени[яй]|правок) код[а]?/,
|
|
12
|
+
/только анализ/,
|
|
13
|
+
/только планирован/,
|
|
14
|
+
].some((pattern) => pattern.test(text));
|
|
15
|
+
if (!noCodePlanning)
|
|
16
|
+
return false;
|
|
17
|
+
return [
|
|
18
|
+
/\b(?:create|open|file|add|split|decompose|break down)[^\n.]{0,100}\b(?:child issues|follow-?up issues|issues|tickets|tasks)\b/,
|
|
19
|
+
/\b(?:child issues|follow-?up issues|issues|tickets|tasks)[^\n.]{0,100}\b(?:create|open|file|add|split|decompose|break down)\b/,
|
|
20
|
+
/(?:поставь|создай|заведи|добавь|разбей)[^\n.]{0,100}(?:задач|тикет|issue)/,
|
|
21
|
+
/(?:задач|тикет|issue)[^\n.]{0,100}(?:поставь|создай|заведи|добавь|разбей)/,
|
|
22
|
+
].some((pattern) => pattern.test(text));
|
|
23
|
+
}
|
|
1
24
|
export function classifyIssue(params) {
|
|
2
25
|
if (params.issue.parentLinearIssueId) {
|
|
3
26
|
return { issueClass: "implementation", issueClassSource: "hierarchy" };
|
|
@@ -14,5 +37,8 @@ export function classifyIssue(params) {
|
|
|
14
37
|
if (params.issue.issueClassSource === "triage" && params.issue.issueClass) {
|
|
15
38
|
return { issueClass: params.issue.issueClass, issueClassSource: "triage" };
|
|
16
39
|
}
|
|
40
|
+
if (hasExplicitNoCodePlanningSplitIntent(params.issue)) {
|
|
41
|
+
return { issueClass: "orchestration", issueClassSource: "heuristic" };
|
|
42
|
+
}
|
|
17
43
|
return { issueClass: "implementation", issueClassSource: "heuristic" };
|
|
18
44
|
}
|
|
@@ -6,7 +6,7 @@ export function computeOrchestrationSettleUntil(now = Date.now()) {
|
|
|
6
6
|
function resolveOrchestrationIssueClass(db, issue) {
|
|
7
7
|
return classifyIssue({
|
|
8
8
|
issue,
|
|
9
|
-
childIssueCount: db.issues.
|
|
9
|
+
childIssueCount: db.issues.listCanonicalChildIssues(issue.projectId, issue.linearIssueId).length,
|
|
10
10
|
}).issueClass;
|
|
11
11
|
}
|
|
12
12
|
function unique(values) {
|
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);
|
|
@@ -186,7 +186,7 @@ export class RunOrchestrator {
|
|
|
186
186
|
...(entry.blockerCurrentLinearStateType ? { stateType: entry.blockerCurrentLinearStateType } : {}),
|
|
187
187
|
}));
|
|
188
188
|
const childIssues = this.db.issues
|
|
189
|
-
.
|
|
189
|
+
.listCanonicalChildIssues(issue.projectId, issue.linearIssueId)
|
|
190
190
|
.map((entry) => ({
|
|
191
191
|
linearIssueId: entry.linearIssueId,
|
|
192
192
|
...(entry.issueKey ? { issueKey: entry.issueKey } : {}),
|
|
@@ -205,7 +205,7 @@ export class RunOrchestrator {
|
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
207
|
async classifyTrackedIssue(issue) {
|
|
208
|
-
const childIssues = this.db.issues.
|
|
208
|
+
const childIssues = this.db.issues.listCanonicalChildIssues(issue.projectId, issue.linearIssueId);
|
|
209
209
|
const classification = classifyIssue({ issue, childIssueCount: childIssues.length });
|
|
210
210
|
const triageHash = buildIssueTriageHash({ issue, childIssues });
|
|
211
211
|
const triageCacheFresh = issue.issueClassSource === "triage" && issue.issueTriageHash === triageHash;
|
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);
|
|
@@ -30,7 +30,7 @@ export class CommentWakeHandler {
|
|
|
30
30
|
return;
|
|
31
31
|
const issueClass = classifyIssue({
|
|
32
32
|
issue,
|
|
33
|
-
childIssueCount: this.db.issues.
|
|
33
|
+
childIssueCount: this.db.issues.listCanonicalChildIssues(project.id, normalized.issue.id).length,
|
|
34
34
|
}).issueClass;
|
|
35
35
|
const trimmedBody = normalized.comment.body.trim();
|
|
36
36
|
const installation = this.db.linearInstallations.getLinearInstallationForProject(project.id);
|
|
@@ -75,7 +75,7 @@ export class DesiredStageRecorder {
|
|
|
75
75
|
terminal,
|
|
76
76
|
currentState: existingIssue?.factoryState,
|
|
77
77
|
});
|
|
78
|
-
const childIssueCount = this.db.issues.
|
|
78
|
+
const childIssueCount = this.db.issues.listCanonicalChildIssues(params.project.id, normalizedIssue.id).length;
|
|
79
79
|
const classification = classifyIssue({
|
|
80
80
|
issue: {
|
|
81
81
|
issueClass: existingIssue?.issueClass,
|