patchrelay 0.36.7 → 0.36.9
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/cli/cluster-health.js +2 -2
- package/dist/cli/data.js +20 -20
- package/dist/db/issue-session-store.js +284 -0
- package/dist/db/issue-store.js +559 -0
- package/dist/db/run-store.js +125 -0
- package/dist/db/webhook-event-store.js +71 -0
- package/dist/db.js +52 -1138
- package/dist/github-webhook-handler.js +44 -44
- package/dist/idle-reconciliation.js +20 -20
- package/dist/interrupted-run-recovery.js +176 -0
- package/dist/issue-query-service.js +13 -13
- package/dist/issue-session-lease-service.js +143 -0
- package/dist/issue-session-projector.js +114 -0
- package/dist/linear-session-sync.js +10 -10
- package/dist/queue-health-monitor.js +5 -5
- package/dist/run-completion-policy.js +412 -0
- package/dist/run-finalizer.js +172 -0
- package/dist/run-launcher.js +193 -0
- package/dist/run-orchestrator.js +145 -1505
- package/dist/run-recovery-service.js +209 -0
- package/dist/run-wake-planner.js +101 -0
- package/dist/service.js +33 -33
- package/dist/tracked-issue-projector.js +69 -0
- package/dist/webhook-handler.js +64 -693
- package/dist/webhooks/agent-session-handler.js +212 -0
- package/dist/webhooks/comment-policy.js +41 -0
- package/dist/webhooks/comment-wake-handler.js +133 -0
- package/dist/webhooks/decision-helpers.js +74 -0
- package/dist/webhooks/desired-stage-recorder.js +177 -0
- package/dist/webhooks/issue-removal-handler.js +68 -0
- package/dist/worktree-manager.js +69 -0
- package/package.json +1 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { buildHookEnv, runProjectHook } from "./hook-runner.js";
|
|
2
|
+
import { buildRunFailureActivity } from "./linear-session-reporting.js";
|
|
3
|
+
import { loadPatchRelayRepoPrompting } from "./patchrelay-customization.js";
|
|
4
|
+
import { buildRunPrompt as buildPatchRelayRunPrompt, findDisallowedPatchRelayPromptSectionIds, findUnknownPatchRelayPromptSectionIds, mergePromptCustomizationLayers, resolvePromptLayers, } from "./prompting/patchrelay.js";
|
|
5
|
+
import { execCommand } from "./utils.js";
|
|
6
|
+
function slugify(value) {
|
|
7
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
8
|
+
}
|
|
9
|
+
function sanitizePathSegment(value) {
|
|
10
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
11
|
+
}
|
|
12
|
+
function shouldCompactThread(issue, threadGeneration, context) {
|
|
13
|
+
const followUpCount = typeof context?.followUpCount === "number" ? context.followUpCount : 0;
|
|
14
|
+
return issue.threadId !== undefined
|
|
15
|
+
&& (threadGeneration ?? 0) >= 4
|
|
16
|
+
&& followUpCount >= 4;
|
|
17
|
+
}
|
|
18
|
+
export function shouldReuseIssueThread(params) {
|
|
19
|
+
return Boolean(params.existingThreadId) && !params.compactThread && params.resumeThread;
|
|
20
|
+
}
|
|
21
|
+
export class RunLauncher {
|
|
22
|
+
config;
|
|
23
|
+
db;
|
|
24
|
+
codex;
|
|
25
|
+
logger;
|
|
26
|
+
worktreeManager;
|
|
27
|
+
constructor(config, db, codex, logger, worktreeManager) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.db = db;
|
|
30
|
+
this.codex = codex;
|
|
31
|
+
this.logger = logger;
|
|
32
|
+
this.worktreeManager = worktreeManager;
|
|
33
|
+
}
|
|
34
|
+
prepareLaunchPlan(params) {
|
|
35
|
+
const repoPrompting = loadPatchRelayRepoPrompting({
|
|
36
|
+
repoRoot: params.project.repoPath,
|
|
37
|
+
logger: this.logger,
|
|
38
|
+
});
|
|
39
|
+
const promptLayer = mergePromptCustomizationLayers(resolvePromptLayers(this.config.prompting, params.runType), resolvePromptLayers(repoPrompting, params.runType));
|
|
40
|
+
const unknownPromptSections = findUnknownPatchRelayPromptSectionIds(promptLayer);
|
|
41
|
+
if (unknownPromptSections.length > 0) {
|
|
42
|
+
this.logger.warn({ issueKey: params.issue.issueKey, runType: params.runType, unknownPromptSections }, "PatchRelay prompt customization references unknown section ids");
|
|
43
|
+
}
|
|
44
|
+
const disallowedPromptSections = findDisallowedPatchRelayPromptSectionIds(promptLayer);
|
|
45
|
+
if (disallowedPromptSections.length > 0) {
|
|
46
|
+
this.logger.warn({ issueKey: params.issue.issueKey, runType: params.runType, disallowedPromptSections }, "PatchRelay prompt customization attempted to replace non-overridable sections");
|
|
47
|
+
}
|
|
48
|
+
const prompt = buildPatchRelayRunPrompt({
|
|
49
|
+
issue: params.issue,
|
|
50
|
+
runType: params.runType,
|
|
51
|
+
repoPath: params.project.repoPath,
|
|
52
|
+
...(params.effectiveContext ? { context: params.effectiveContext } : {}),
|
|
53
|
+
...(promptLayer ? { promptLayer } : {}),
|
|
54
|
+
});
|
|
55
|
+
const issueRef = sanitizePathSegment(params.issue.issueKey ?? params.issue.linearIssueId);
|
|
56
|
+
const slug = params.issue.title ? slugify(params.issue.title) : "";
|
|
57
|
+
const branchSuffix = slug ? `${issueRef}-${slug}` : issueRef;
|
|
58
|
+
const branchName = params.issue.branchName ?? `${params.project.branchPrefix}/${branchSuffix}`;
|
|
59
|
+
const worktreePath = params.issue.worktreePath ?? `${params.project.worktreeRoot}/${issueRef}`;
|
|
60
|
+
return { prompt, branchName, worktreePath };
|
|
61
|
+
}
|
|
62
|
+
claimRun(params) {
|
|
63
|
+
return this.db.issueSessions.withIssueSessionLease(params.item.projectId, params.item.issueId, params.leaseId, () => {
|
|
64
|
+
const fresh = this.db.issues.getIssue(params.item.projectId, params.item.issueId);
|
|
65
|
+
if (!fresh || fresh.activeRunId !== undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
const wakeIssue = params.materializeLegacyPendingWake(fresh, {
|
|
68
|
+
projectId: params.item.projectId,
|
|
69
|
+
linearIssueId: params.item.issueId,
|
|
70
|
+
leaseId: params.leaseId,
|
|
71
|
+
});
|
|
72
|
+
const freshWake = params.resolveRunWake(wakeIssue);
|
|
73
|
+
if (!freshWake || freshWake.runType !== params.runType)
|
|
74
|
+
return undefined;
|
|
75
|
+
const created = this.db.runs.createRun({
|
|
76
|
+
issueId: fresh.id,
|
|
77
|
+
projectId: params.item.projectId,
|
|
78
|
+
linearIssueId: params.item.issueId,
|
|
79
|
+
runType: params.runType,
|
|
80
|
+
...(params.sourceHeadSha ? { sourceHeadSha: params.sourceHeadSha } : {}),
|
|
81
|
+
promptText: params.prompt,
|
|
82
|
+
});
|
|
83
|
+
const failureHeadSha = typeof params.effectiveContext?.failureHeadSha === "string"
|
|
84
|
+
? params.effectiveContext.failureHeadSha
|
|
85
|
+
: typeof params.effectiveContext?.headSha === "string" ? params.effectiveContext.headSha : undefined;
|
|
86
|
+
const failureSignature = typeof params.effectiveContext?.failureSignature === "string" ? params.effectiveContext.failureSignature : undefined;
|
|
87
|
+
this.db.issues.upsertIssue({
|
|
88
|
+
projectId: params.item.projectId,
|
|
89
|
+
linearIssueId: params.item.issueId,
|
|
90
|
+
pendingRunType: null,
|
|
91
|
+
pendingRunContextJson: null,
|
|
92
|
+
activeRunId: created.id,
|
|
93
|
+
branchName: params.branchName,
|
|
94
|
+
worktreePath: params.worktreePath,
|
|
95
|
+
factoryState: params.runType === "implementation" ? "implementing"
|
|
96
|
+
: params.runType === "ci_repair" ? "repairing_ci"
|
|
97
|
+
: params.runType === "review_fix" || params.runType === "branch_upkeep" ? "changes_requested"
|
|
98
|
+
: params.runType === "queue_repair" ? "repairing_queue"
|
|
99
|
+
: "implementing",
|
|
100
|
+
...((params.runType === "ci_repair" || params.runType === "queue_repair") && failureSignature
|
|
101
|
+
? {
|
|
102
|
+
lastAttemptedFailureSignature: failureSignature,
|
|
103
|
+
lastAttemptedFailureHeadSha: failureHeadSha ?? null,
|
|
104
|
+
}
|
|
105
|
+
: {}),
|
|
106
|
+
});
|
|
107
|
+
this.db.issueSessions.consumeIssueSessionEvents(params.item.projectId, params.item.issueId, freshWake.eventIds, created.id);
|
|
108
|
+
this.db.issueSessions.setIssueSessionLastWakeReason(params.item.projectId, params.item.issueId, freshWake.wakeReason ?? null);
|
|
109
|
+
this.db.issueSessions.setBranchOwnerWithLease({ projectId: params.item.projectId, linearIssueId: params.item.issueId, leaseId: params.leaseId }, "patchrelay");
|
|
110
|
+
return created;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async launchTurn(params) {
|
|
114
|
+
let threadId;
|
|
115
|
+
let turnId;
|
|
116
|
+
let parentThreadId;
|
|
117
|
+
try {
|
|
118
|
+
await this.worktreeManager.ensureIssueWorktree(params.project.repoPath, params.project.worktreeRoot, params.worktreePath, params.branchName, { allowExistingOutsideRoot: params.issue.branchName !== undefined });
|
|
119
|
+
if (params.botIdentity) {
|
|
120
|
+
const gitBin = this.config.runner.gitBin;
|
|
121
|
+
await execCommand(gitBin, ["-C", params.worktreePath, "config", "user.name", params.botIdentity.name], { timeoutMs: 5_000 });
|
|
122
|
+
await execCommand(gitBin, ["-C", params.worktreePath, "config", "user.email", params.botIdentity.email], { timeoutMs: 5_000 });
|
|
123
|
+
const credentialHelper = `!f() { echo "username=x-access-token"; echo "password=$(cat ${params.botIdentity.tokenFile})"; }; f`;
|
|
124
|
+
await execCommand(gitBin, ["-C", params.worktreePath, "config", "credential.helper", credentialHelper], { timeoutMs: 5_000 });
|
|
125
|
+
}
|
|
126
|
+
await this.worktreeManager.resetWorktreeToTrackedBranch(params.worktreePath, params.branchName, params.issue, this.logger);
|
|
127
|
+
if (params.runType !== "queue_repair") {
|
|
128
|
+
await this.worktreeManager.freshenWorktree(params.worktreePath, params.project, params.issue, this.logger);
|
|
129
|
+
}
|
|
130
|
+
const hookEnv = buildHookEnv(params.issue.issueKey ?? params.issue.linearIssueId, params.branchName, params.runType, params.worktreePath);
|
|
131
|
+
const prepareResult = await runProjectHook(params.project.repoPath, "prepare-worktree", { cwd: params.worktreePath, env: hookEnv });
|
|
132
|
+
if (prepareResult.ran && prepareResult.exitCode !== 0) {
|
|
133
|
+
throw new Error(`prepare-worktree hook failed (exit ${prepareResult.exitCode}): ${prepareResult.stderr?.slice(0, 500) ?? ""}`);
|
|
134
|
+
}
|
|
135
|
+
params.assertLaunchLease(params.run, "before starting the Codex turn");
|
|
136
|
+
const compactThread = shouldCompactThread(params.issue, params.issueSession?.threadGeneration, params.effectiveContext);
|
|
137
|
+
if (compactThread && params.issue.threadId) {
|
|
138
|
+
parentThreadId = params.issue.threadId;
|
|
139
|
+
}
|
|
140
|
+
if (shouldReuseIssueThread({ existingThreadId: params.issue.threadId, compactThread, resumeThread: params.resumeThread })) {
|
|
141
|
+
threadId = params.issue.threadId;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const thread = await this.codex.startThread({ cwd: params.worktreePath });
|
|
145
|
+
threadId = thread.id;
|
|
146
|
+
this.db.issueSessions.upsertIssueWithLease({ projectId: params.project.id, linearIssueId: params.issue.linearIssueId, leaseId: params.leaseId }, { projectId: params.project.id, linearIssueId: params.issue.linearIssueId, threadId });
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const turn = await this.codex.startTurn({ threadId, cwd: params.worktreePath, input: params.prompt });
|
|
150
|
+
turnId = turn.turnId;
|
|
151
|
+
}
|
|
152
|
+
catch (turnError) {
|
|
153
|
+
const msg = turnError instanceof Error ? turnError.message : String(turnError);
|
|
154
|
+
if (msg.includes("thread not found") || msg.includes("not materialized")) {
|
|
155
|
+
this.logger.info({ issueKey: params.issue.issueKey, staleThreadId: threadId }, "Thread is stale, retrying with fresh thread");
|
|
156
|
+
const thread = await this.codex.startThread({ cwd: params.worktreePath });
|
|
157
|
+
threadId = thread.id;
|
|
158
|
+
this.db.issueSessions.upsertIssueWithLease({ projectId: params.project.id, linearIssueId: params.issue.linearIssueId, leaseId: params.leaseId }, { projectId: params.project.id, linearIssueId: params.issue.linearIssueId, threadId });
|
|
159
|
+
const turn = await this.codex.startTurn({ threadId, cwd: params.worktreePath, input: params.prompt });
|
|
160
|
+
turnId = turn.turnId;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
throw turnError;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
params.assertLaunchLease(params.run, "after starting the Codex turn");
|
|
167
|
+
return { threadId, turnId, ...(parentThreadId ? { parentThreadId } : {}) };
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
171
|
+
const lostLease = error instanceof Error && error.name === "IssueSessionLeaseLostError";
|
|
172
|
+
if (!lostLease) {
|
|
173
|
+
const nextState = params.isRequestedChangesRunType(params.runType) ? "escalated" : "failed";
|
|
174
|
+
this.db.issueSessions.finishRunWithLease({ projectId: params.project.id, linearIssueId: params.issue.linearIssueId, leaseId: params.leaseId }, params.run.id, {
|
|
175
|
+
status: "failed",
|
|
176
|
+
failureReason: message,
|
|
177
|
+
});
|
|
178
|
+
this.db.issueSessions.upsertIssueWithLease({ projectId: params.project.id, linearIssueId: params.issue.linearIssueId, leaseId: params.leaseId }, {
|
|
179
|
+
projectId: params.project.id,
|
|
180
|
+
linearIssueId: params.issue.linearIssueId,
|
|
181
|
+
activeRunId: null,
|
|
182
|
+
factoryState: nextState,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
this.logger.error({ issueKey: params.issue.issueKey, runType: params.runType, error: message }, `Failed to launch ${params.runType} run`);
|
|
186
|
+
const failedIssue = this.db.issues.getIssue(params.project.id, params.issue.linearIssueId) ?? params.issue;
|
|
187
|
+
void params.linearSync.emitActivity(failedIssue, buildRunFailureActivity(params.runType, `Failed to start ${params.lowerCaseFirst(message)}`));
|
|
188
|
+
void params.linearSync.syncSession(failedIssue, { activeRunType: params.runType });
|
|
189
|
+
params.releaseLease(params.project.id, params.issue.linearIssueId);
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|