patchrelay 0.67.2 → 0.68.0

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.67.2",
4
- "commit": "8d3d15a51ae1",
5
- "builtAt": "2026-05-10T15:37:09.721Z"
3
+ "version": "0.68.0",
4
+ "commit": "3a86b2f7100f",
5
+ "builtAt": "2026-05-13T20:52:25.195Z"
6
6
  }
@@ -206,6 +206,20 @@ export class CodexAppServerClient extends EventEmitter {
206
206
  status: String(response.turn.status),
207
207
  };
208
208
  }
209
+ async setThreadGoal(options) {
210
+ const params = {
211
+ threadId: options.threadId,
212
+ objective: options.objective,
213
+ };
214
+ if (options.status !== undefined) {
215
+ params.status = options.status;
216
+ }
217
+ if (options.tokenBudget !== undefined) {
218
+ params.tokenBudget = options.tokenBudget;
219
+ }
220
+ const response = (await this.sendRequest("thread/goal/set", params));
221
+ return this.mapThreadGoal(response.goal);
222
+ }
209
223
  async readThread(threadId, includeTurns = true) {
210
224
  const response = (await this.sendRequest("thread/read", {
211
225
  threadId,
@@ -230,6 +244,18 @@ export class CodexAppServerClient extends EventEmitter {
230
244
  ],
231
245
  });
232
246
  }
247
+ mapThreadGoal(goal) {
248
+ return {
249
+ threadId: String(goal.threadId),
250
+ objective: String(goal.objective ?? ""),
251
+ status: String(goal.status ?? "active"),
252
+ ...(goal.tokenBudget === null || goal.tokenBudget === undefined ? { tokenBudget: null } : { tokenBudget: Number(goal.tokenBudget) }),
253
+ tokensUsed: Number(goal.tokensUsed ?? 0),
254
+ timeUsedSeconds: Number(goal.timeUsedSeconds ?? 0),
255
+ createdAt: Number(goal.createdAt ?? 0),
256
+ updatedAt: Number(goal.updatedAt ?? 0),
257
+ };
258
+ }
233
259
  sendNotification(method, params) {
234
260
  this.writeMessage({
235
261
  jsonrpc: "2.0",
@@ -3,6 +3,7 @@ import { buildRunFailureActivity } from "./linear-session-reporting.js";
3
3
  import { loadPatchRelayRepoPrompting } from "./patchrelay-customization.js";
4
4
  import { buildRunPrompt as buildPatchRelayRunPrompt, findDisallowedPatchRelayPromptSectionIds, findUnknownPatchRelayPromptSectionIds, mergePromptCustomizationLayers, resolvePromptLayers, } from "./prompting/patchrelay.js";
5
5
  import { configureGitHubBotAuthForWorktree } from "./github-worktree-auth.js";
6
+ import { sanitizeDiagnosticText } from "./utils.js";
6
7
  function slugify(value) {
7
8
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
8
9
  }
@@ -15,6 +16,28 @@ function shouldCompactThread(issue, threadGeneration, context) {
15
16
  && (threadGeneration ?? 0) >= 4
16
17
  && followUpCount >= 4;
17
18
  }
19
+ function compactGoalText(value, maxLength = 600) {
20
+ const normalized = value.replace(/\s+/g, " ").trim();
21
+ return normalized.length <= maxLength ? normalized : `${normalized.slice(0, maxLength - 3).trimEnd()}...`;
22
+ }
23
+ function extractIssueSection(description, heading) {
24
+ if (!description)
25
+ return undefined;
26
+ const headingLine = `## ${heading}`.toLowerCase();
27
+ const lines = description.split(/\r?\n/);
28
+ const start = lines.findIndex((line) => line.trim().toLowerCase() === headingLine);
29
+ if (start === -1)
30
+ return undefined;
31
+ const end = lines.findIndex((line, index) => index > start && /^##\s+/.test(line));
32
+ const body = lines.slice(start + 1, end === -1 ? undefined : end).join("\n").trim();
33
+ return body && body.length > 0 ? body : undefined;
34
+ }
35
+ export function buildInitialImplementationGoal(issue) {
36
+ const title = issue.title?.trim() || `Complete ${issue.issueKey ?? issue.linearIssueId}`;
37
+ const description = issue.description?.trim();
38
+ const goal = extractIssueSection(description, "Goal");
39
+ return compactGoalText(goal ? `${title}. ${goal}` : title);
40
+ }
18
41
  export function shouldReuseIssueThread(params) {
19
42
  return Boolean(params.existingThreadId) && !params.compactThread && params.resumeThread;
20
43
  }
@@ -125,6 +148,8 @@ export class RunLauncher {
125
148
  let threadId;
126
149
  let turnId;
127
150
  let parentThreadId;
151
+ let createdThreadForRun = false;
152
+ const firstThreadForIssue = !params.issue.threadId;
128
153
  try {
129
154
  await this.worktreeManager.ensureIssueWorktree(params.project.repoPath, params.project.worktreeRoot, params.worktreePath, params.branchName, { allowExistingOutsideRoot: params.issue.branchName !== undefined });
130
155
  if (params.botIdentity) {
@@ -157,6 +182,7 @@ export class RunLauncher {
157
182
  else {
158
183
  const thread = await this.codex.startThread({ cwd: params.worktreePath });
159
184
  threadId = thread.id;
185
+ createdThreadForRun = true;
160
186
  this.db.issueSessions.upsertIssueWithLease({ projectId: params.project.id, linearIssueId: params.issue.linearIssueId, leaseId: params.leaseId }, { projectId: params.project.id, linearIssueId: params.issue.linearIssueId, threadId });
161
187
  }
162
188
  try {
@@ -169,6 +195,7 @@ export class RunLauncher {
169
195
  this.logger.info({ issueKey: params.issue.issueKey, staleThreadId: threadId }, "Thread is stale, retrying with fresh thread");
170
196
  const thread = await this.codex.startThread({ cwd: params.worktreePath });
171
197
  threadId = thread.id;
198
+ createdThreadForRun = true;
172
199
  this.db.issueSessions.upsertIssueWithLease({ projectId: params.project.id, linearIssueId: params.issue.linearIssueId, leaseId: params.leaseId }, { projectId: params.project.id, linearIssueId: params.issue.linearIssueId, threadId });
173
200
  const turn = await this.codex.startTurn({ threadId, cwd: params.worktreePath, input: params.prompt });
174
201
  turnId = turn.turnId;
@@ -177,6 +204,9 @@ export class RunLauncher {
177
204
  throw turnError;
178
205
  }
179
206
  }
207
+ if (createdThreadForRun && firstThreadForIssue && params.runType === "implementation") {
208
+ await this.setInitialImplementationGoal(threadId, params.issue);
209
+ }
180
210
  params.assertLaunchLease(params.run, "after starting the Codex turn");
181
211
  return { threadId, turnId, ...(parentThreadId ? { parentThreadId } : {}) };
182
212
  }
@@ -204,4 +234,22 @@ export class RunLauncher {
204
234
  throw error;
205
235
  }
206
236
  }
237
+ async setInitialImplementationGoal(threadId, issue) {
238
+ const goalSetter = this.codex.setThreadGoal;
239
+ if (typeof goalSetter !== "function") {
240
+ return;
241
+ }
242
+ const objective = buildInitialImplementationGoal(issue);
243
+ try {
244
+ await goalSetter.call(this.codex, { threadId, objective, status: "active" });
245
+ this.logger.info({ issueKey: issue.issueKey, threadId }, "Set Codex thread goal for implementation run");
246
+ }
247
+ catch (error) {
248
+ this.logger.warn({
249
+ issueKey: issue.issueKey,
250
+ threadId,
251
+ error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
252
+ }, "Failed to set Codex thread goal for implementation run");
253
+ }
254
+ }
207
255
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.67.2",
3
+ "version": "0.68.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {