patchrelay 0.36.6 → 0.36.8
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/README.md +3 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/cluster-health.js +8 -8
- package/dist/cli/commands/setup.js +32 -27
- package/dist/cli/data.js +11 -11
- package/dist/cli/help.js +1 -1
- package/dist/cli/service-commands.js +11 -0
- package/dist/config.js +48 -0
- package/dist/db/issue-session-store.js +292 -0
- package/dist/db/run-store.js +127 -0
- package/dist/db/webhook-event-store.js +71 -0
- package/dist/db.js +22 -520
- package/dist/github-webhook-handler.js +25 -25
- package/dist/idle-reconciliation.js +5 -5
- package/dist/issue-query-service.js +9 -9
- package/dist/issue-session-lease-service.js +143 -0
- package/dist/linear-session-sync.js +4 -4
- package/dist/patchrelay-customization.js +68 -0
- package/dist/prompting/patchrelay.js +552 -0
- package/dist/queue-health-monitor.js +2 -2
- package/dist/run-finalizer.js +161 -0
- package/dist/run-launcher.js +193 -0
- package/dist/run-orchestrator.js +151 -1396
- package/dist/run-recovery-service.js +203 -0
- package/dist/run-wake-planner.js +101 -0
- package/dist/service.js +24 -24
- package/dist/tracked-issue-projector.js +69 -0
- package/dist/webhook-handler.js +59 -688
- 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/package.json +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { buildStageReport, countEventMethods } from "./run-reporting.js";
|
|
2
|
+
import { buildRunCompletedActivity, buildRunFailureActivity } from "./linear-session-reporting.js";
|
|
3
|
+
export class RunFinalizer {
|
|
4
|
+
db;
|
|
5
|
+
logger;
|
|
6
|
+
linearSync;
|
|
7
|
+
enqueueIssue;
|
|
8
|
+
feed;
|
|
9
|
+
constructor(db, logger, linearSync, enqueueIssue, feed) {
|
|
10
|
+
this.db = db;
|
|
11
|
+
this.logger = logger;
|
|
12
|
+
this.linearSync = linearSync;
|
|
13
|
+
this.enqueueIssue = enqueueIssue;
|
|
14
|
+
this.feed = feed;
|
|
15
|
+
}
|
|
16
|
+
async finalizeCompletedRun(params) {
|
|
17
|
+
const { run, issue, thread, threadId } = params;
|
|
18
|
+
const trackedIssue = this.db.issueToTrackedIssue(issue);
|
|
19
|
+
const report = buildStageReport({ ...run, status: "completed" }, trackedIssue, thread, countEventMethods(this.db.runs.listThreadEvents(run.id)));
|
|
20
|
+
const freshIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
21
|
+
const verifiedRepairError = await params.verifyReactiveRunAdvancedBranch(run, freshIssue);
|
|
22
|
+
if (verifiedRepairError) {
|
|
23
|
+
const holdState = params.resolveRecoverableRunState(freshIssue) ?? "failed";
|
|
24
|
+
params.failRunAndClear(run, verifiedRepairError, holdState);
|
|
25
|
+
const heldIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? freshIssue;
|
|
26
|
+
this.feed?.publish({
|
|
27
|
+
level: "warn",
|
|
28
|
+
kind: "turn",
|
|
29
|
+
issueKey: freshIssue.issueKey,
|
|
30
|
+
projectId: run.projectId,
|
|
31
|
+
stage: run.runType,
|
|
32
|
+
status: "branch_not_advanced",
|
|
33
|
+
summary: verifiedRepairError,
|
|
34
|
+
});
|
|
35
|
+
void this.linearSync.emitActivity(heldIssue, buildRunFailureActivity(run.runType, verifiedRepairError));
|
|
36
|
+
void this.linearSync.syncSession(heldIssue, { activeRunType: run.runType });
|
|
37
|
+
this.linearSync.clearProgress(run.id);
|
|
38
|
+
params.releaseLease(run.projectId, run.linearIssueId);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const missingReviewFixHeadError = await params.verifyReviewFixAdvancedHead(run, freshIssue);
|
|
42
|
+
if (missingReviewFixHeadError) {
|
|
43
|
+
params.failRunAndClear(run, missingReviewFixHeadError, "escalated");
|
|
44
|
+
const failedIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? freshIssue;
|
|
45
|
+
this.feed?.publish({
|
|
46
|
+
level: "error",
|
|
47
|
+
kind: "turn",
|
|
48
|
+
issueKey: freshIssue.issueKey,
|
|
49
|
+
projectId: run.projectId,
|
|
50
|
+
stage: run.runType,
|
|
51
|
+
status: "same_head_review_handoff_blocked",
|
|
52
|
+
summary: missingReviewFixHeadError,
|
|
53
|
+
});
|
|
54
|
+
void this.linearSync.emitActivity(failedIssue, buildRunFailureActivity(run.runType, missingReviewFixHeadError));
|
|
55
|
+
void this.linearSync.syncSession(failedIssue, { activeRunType: run.runType });
|
|
56
|
+
this.linearSync.clearProgress(run.id);
|
|
57
|
+
params.releaseLease(run.projectId, run.linearIssueId);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const publishedOutcomeError = await params.verifyPublishedRunOutcome(run, freshIssue);
|
|
61
|
+
if (publishedOutcomeError) {
|
|
62
|
+
params.failRunAndClear(run, publishedOutcomeError, "failed");
|
|
63
|
+
const failedIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? freshIssue;
|
|
64
|
+
this.feed?.publish({
|
|
65
|
+
level: "warn",
|
|
66
|
+
kind: "turn",
|
|
67
|
+
issueKey: freshIssue.issueKey,
|
|
68
|
+
projectId: run.projectId,
|
|
69
|
+
stage: run.runType,
|
|
70
|
+
status: "publish_incomplete",
|
|
71
|
+
summary: publishedOutcomeError,
|
|
72
|
+
});
|
|
73
|
+
void this.linearSync.emitActivity(failedIssue, buildRunFailureActivity(run.runType, publishedOutcomeError));
|
|
74
|
+
void this.linearSync.syncSession(failedIssue, { activeRunType: run.runType });
|
|
75
|
+
this.linearSync.clearProgress(run.id);
|
|
76
|
+
if (params.source === "notification") {
|
|
77
|
+
params.releaseLease(run.projectId, run.linearIssueId);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const refreshedIssue = await params.refreshIssueAfterReactivePublish(run, freshIssue);
|
|
82
|
+
const postRunFollowUp = await params.resolvePostRunFollowUp(run, refreshedIssue);
|
|
83
|
+
const postRunState = postRunFollowUp?.factoryState ?? params.resolveCompletedRunState(refreshedIssue, run);
|
|
84
|
+
const completed = params.withHeldLease(run.projectId, run.linearIssueId, (lease) => {
|
|
85
|
+
this.db.runs.finishRun(run.id, {
|
|
86
|
+
status: "completed",
|
|
87
|
+
threadId,
|
|
88
|
+
...(params.completedTurnId ? { turnId: params.completedTurnId } : {}),
|
|
89
|
+
summaryJson: JSON.stringify({ latestAssistantMessage: report.assistantMessages.at(-1) ?? null }),
|
|
90
|
+
reportJson: JSON.stringify(report),
|
|
91
|
+
});
|
|
92
|
+
this.db.upsertIssue({
|
|
93
|
+
projectId: run.projectId,
|
|
94
|
+
linearIssueId: run.linearIssueId,
|
|
95
|
+
activeRunId: null,
|
|
96
|
+
...(postRunState ? { factoryState: postRunState } : {}),
|
|
97
|
+
pendingRunType: null,
|
|
98
|
+
pendingRunContextJson: null,
|
|
99
|
+
...(postRunFollowUp ? {} : (postRunState === "awaiting_queue" || postRunState === "done"
|
|
100
|
+
? {
|
|
101
|
+
lastGitHubFailureSource: null,
|
|
102
|
+
lastGitHubFailureHeadSha: null,
|
|
103
|
+
lastGitHubFailureSignature: null,
|
|
104
|
+
lastGitHubFailureCheckName: null,
|
|
105
|
+
lastGitHubFailureCheckUrl: null,
|
|
106
|
+
lastGitHubFailureContextJson: null,
|
|
107
|
+
lastGitHubFailureAt: null,
|
|
108
|
+
lastQueueIncidentJson: null,
|
|
109
|
+
lastAttemptedFailureHeadSha: null,
|
|
110
|
+
lastAttemptedFailureSignature: null,
|
|
111
|
+
}
|
|
112
|
+
: {})),
|
|
113
|
+
});
|
|
114
|
+
if (postRunFollowUp) {
|
|
115
|
+
return params.appendWakeEventWithLease(lease, issue, postRunFollowUp.pendingRunType, postRunFollowUp.context, "post_run");
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
});
|
|
119
|
+
if (!completed) {
|
|
120
|
+
this.logger.warn({ runId: run.id, issueId: run.linearIssueId }, "Skipping completion writes after losing issue-session lease");
|
|
121
|
+
this.linearSync.clearProgress(run.id);
|
|
122
|
+
params.releaseLease(run.projectId, run.linearIssueId);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (postRunFollowUp) {
|
|
126
|
+
this.feed?.publish({
|
|
127
|
+
level: "info",
|
|
128
|
+
kind: "stage",
|
|
129
|
+
issueKey: issue.issueKey,
|
|
130
|
+
projectId: run.projectId,
|
|
131
|
+
stage: postRunFollowUp.factoryState,
|
|
132
|
+
status: "follow_up_queued",
|
|
133
|
+
summary: postRunFollowUp.summary,
|
|
134
|
+
});
|
|
135
|
+
this.enqueueIssue(run.projectId, run.linearIssueId);
|
|
136
|
+
}
|
|
137
|
+
this.feed?.publish({
|
|
138
|
+
level: "info",
|
|
139
|
+
kind: "turn",
|
|
140
|
+
issueKey: issue.issueKey,
|
|
141
|
+
projectId: run.projectId,
|
|
142
|
+
stage: run.runType,
|
|
143
|
+
status: "completed",
|
|
144
|
+
summary: params.source === "notification"
|
|
145
|
+
? `Turn completed for ${run.runType}`
|
|
146
|
+
: `Reconciliation: ${run.runType} completed${postRunState ? ` -> ${postRunState}` : ""}`,
|
|
147
|
+
...(report.assistantMessages.at(-1) ? { detail: report.assistantMessages.at(-1) } : {}),
|
|
148
|
+
});
|
|
149
|
+
const updatedIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? refreshedIssue;
|
|
150
|
+
const completionSummary = report.assistantMessages.at(-1)?.slice(0, 300) ?? `${run.runType} completed.`;
|
|
151
|
+
void this.linearSync.emitActivity(updatedIssue, buildRunCompletedActivity({
|
|
152
|
+
runType: run.runType,
|
|
153
|
+
completionSummary,
|
|
154
|
+
postRunState: updatedIssue.factoryState,
|
|
155
|
+
...(updatedIssue.prNumber !== undefined ? { prNumber: updatedIssue.prNumber } : {}),
|
|
156
|
+
}));
|
|
157
|
+
void this.linearSync.syncSession(updatedIssue);
|
|
158
|
+
this.linearSync.clearProgress(run.id);
|
|
159
|
+
params.releaseLease(run.projectId, run.linearIssueId);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -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.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.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 params.resetWorktreeToTrackedBranch(params.worktreePath, params.branchName, params.issue);
|
|
127
|
+
if (params.runType !== "queue_repair") {
|
|
128
|
+
await params.freshenWorktree(params.worktreePath, params.project, params.issue);
|
|
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.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
|
+
}
|