patchrelay 0.56.1 → 0.57.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.56.1",
4
- "commit": "39a7bc21b981",
5
- "builtAt": "2026-05-01T15:22:20.307Z"
3
+ "version": "0.57.0",
4
+ "commit": "d67134a3b377",
5
+ "builtAt": "2026-05-01T22:45:14.762Z"
6
6
  }
@@ -0,0 +1,83 @@
1
+ const ACTIVITY_RECOVERY_LIMIT = 20;
2
+ const MAX_CONTEXT_ACTIVITIES = 8;
3
+ const MAX_ACTIVITY_TEXT_LENGTH = 500;
4
+ function trimBounded(value, maxLength = MAX_ACTIVITY_TEXT_LENGTH) {
5
+ const normalized = value.replace(/\s+/g, " ").trim();
6
+ if (normalized.length <= maxLength)
7
+ return normalized;
8
+ return `${normalized.slice(0, maxLength - 1).trimEnd()}...`;
9
+ }
10
+ function hasRecoveredContext(context) {
11
+ return typeof context?.linearAgentActivityContext === "string" && context.linearAgentActivityContext.trim().length > 0;
12
+ }
13
+ function hasLocalHumanContext(context) {
14
+ if (hasRecoveredContext(context))
15
+ return true;
16
+ for (const key of ["promptContext", "promptBody", "operatorPrompt", "userComment"]) {
17
+ const value = context?.[key];
18
+ if (typeof value === "string" && value.trim().length > 0)
19
+ return true;
20
+ }
21
+ if (!Array.isArray(context?.followUps))
22
+ return false;
23
+ return context.followUps.some((entry) => {
24
+ if (!entry || typeof entry !== "object")
25
+ return false;
26
+ const text = entry.text;
27
+ return typeof text === "string" && text.trim().length > 0;
28
+ });
29
+ }
30
+ function activitySortKey(activity) {
31
+ const parsed = activity.updatedAt ? Date.parse(activity.updatedAt) : NaN;
32
+ return Number.isFinite(parsed) ? parsed : 0;
33
+ }
34
+ function describeActivity(activity) {
35
+ const type = activity.type?.trim() || "activity";
36
+ const body = typeof activity.body === "string" ? trimBounded(activity.body) : "";
37
+ if (body) {
38
+ return `${type}: ${body}`;
39
+ }
40
+ if (activity.action || activity.parameter || activity.result) {
41
+ const action = activity.action ? trimBounded(activity.action, 120) : "action";
42
+ const parameter = activity.parameter ? ` ${trimBounded(activity.parameter, 180)}` : "";
43
+ const result = activity.result ? ` -> ${trimBounded(activity.result, 180)}` : "";
44
+ return `${type}: ${action}${parameter}${result}`;
45
+ }
46
+ return undefined;
47
+ }
48
+ export function summarizeLinearAgentActivities(activities) {
49
+ const lines = [...activities]
50
+ .sort((left, right) => activitySortKey(left) - activitySortKey(right))
51
+ .map(describeActivity)
52
+ .filter((line) => Boolean(line))
53
+ .slice(-MAX_CONTEXT_ACTIVITIES);
54
+ if (lines.length === 0)
55
+ return undefined;
56
+ return {
57
+ linearAgentActivityContext: lines.map((line) => `- ${line}`).join("\n"),
58
+ linearAgentActivityCount: lines.length,
59
+ };
60
+ }
61
+ export async function recoverLinearAgentActivityContext(params) {
62
+ if (!params.agentSessionId || hasLocalHumanContext(params.context)) {
63
+ return undefined;
64
+ }
65
+ try {
66
+ const linear = await params.linearProvider.forProject(params.projectId);
67
+ if (!linear?.listAgentSessionActivities) {
68
+ return undefined;
69
+ }
70
+ const activities = await linear.listAgentSessionActivities(params.agentSessionId, {
71
+ first: ACTIVITY_RECOVERY_LIMIT,
72
+ });
73
+ return summarizeLinearAgentActivities(activities);
74
+ }
75
+ catch (error) {
76
+ params.logger.warn({
77
+ issueKey: params.issueKey,
78
+ agentSessionId: params.agentSessionId,
79
+ error: error instanceof Error ? error.message : String(error),
80
+ }, "Failed to recover Linear agent activity context");
81
+ return undefined;
82
+ }
83
+ }
@@ -241,6 +241,55 @@ export class LinearGraphqlClient {
241
241
  }
242
242
  return response.agentSessionUpdate.agentSession;
243
243
  }
244
+ async listAgentSessionActivities(agentSessionId, options) {
245
+ const response = await this.request(`
246
+ query PatchRelayAgentSessionActivities($id: String!, $first: Int!) {
247
+ agentSession(id: $id) {
248
+ activities(first: $first) {
249
+ edges {
250
+ node {
251
+ id
252
+ updatedAt
253
+ content {
254
+ __typename
255
+ ... on AgentActivityThoughtContent {
256
+ body
257
+ }
258
+ ... on AgentActivityActionContent {
259
+ action
260
+ parameter
261
+ result
262
+ }
263
+ ... on AgentActivityElicitationContent {
264
+ body
265
+ }
266
+ ... on AgentActivityResponseContent {
267
+ body
268
+ }
269
+ ... on AgentActivityErrorContent {
270
+ body
271
+ }
272
+ ... on AgentActivityPromptContent {
273
+ body
274
+ }
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }
280
+ }
281
+ `, {
282
+ id: agentSessionId,
283
+ first: Math.max(1, Math.min(options?.first ?? 20, 50)),
284
+ });
285
+ const activityEdges = response.agentSession?.activities?.edges;
286
+ const rawActivities = activityEdges
287
+ ? activityEdges.map((edge) => edge?.node)
288
+ : response.agentSession?.activities?.nodes ?? [];
289
+ return rawActivities
290
+ .filter((activity) => Boolean(activity))
291
+ .map(mapAgentActivity);
292
+ }
244
293
  async updateIssueLabels(params) {
245
294
  const issue = await this.getIssue(params.issueId);
246
295
  const addIds = this.resolveLabelIds(issue, params.addNames ?? []);
@@ -439,6 +488,25 @@ function mapIssueRelation(raw) {
439
488
  ...(raw.state?.type ? { stateType: raw.state.type } : {}),
440
489
  };
441
490
  }
491
+ function mapAgentActivity(raw) {
492
+ const content = raw.content ?? {};
493
+ return {
494
+ id: raw.id,
495
+ ...(content.__typename ? { type: normalizeAgentActivityType(content.__typename) } : {}),
496
+ ...(content.body ? { body: content.body } : {}),
497
+ ...(content.action ? { action: content.action } : {}),
498
+ ...(content.parameter ? { parameter: content.parameter } : {}),
499
+ ...(content.result ? { result: content.result } : {}),
500
+ ...(raw.updatedAt ? { updatedAt: raw.updatedAt } : {}),
501
+ };
502
+ }
503
+ function normalizeAgentActivityType(typename) {
504
+ return typename
505
+ .replace(/^AgentActivity/, "")
506
+ .replace(/Content$/, "")
507
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
508
+ .toLowerCase();
509
+ }
442
510
  export class DatabaseBackedLinearClientProvider {
443
511
  config;
444
512
  db;
@@ -197,6 +197,9 @@ function buildHumanContextLines(context) {
197
197
  const latestPrompt = typeof context?.promptBody === "string" ? context.promptBody.trim() : "";
198
198
  const operatorPrompt = typeof context?.operatorPrompt === "string" ? context.operatorPrompt.trim() : "";
199
199
  const userComment = typeof context?.userComment === "string" ? context.userComment.trim() : "";
200
+ const linearAgentActivityContext = typeof context?.linearAgentActivityContext === "string"
201
+ ? context.linearAgentActivityContext.trim()
202
+ : "";
200
203
  const lines = [];
201
204
  if (promptContext) {
202
205
  lines.push("Linear session context:", promptContext, "");
@@ -210,6 +213,9 @@ function buildHumanContextLines(context) {
210
213
  if (userComment) {
211
214
  lines.push("Human follow-up comment:", userComment, "");
212
215
  }
216
+ if (linearAgentActivityContext) {
217
+ lines.push("Recovered Linear agent activity context:", linearAgentActivityContext, "");
218
+ }
213
219
  return lines;
214
220
  }
215
221
  function resolveRequestedChangesMode(runType, context) {
@@ -8,6 +8,7 @@ import { MainBranchHealthMonitor } from "./main-branch-health-monitor.js";
8
8
  import { QueueHealthMonitor } from "./queue-health-monitor.js";
9
9
  import { IdleIssueReconciler } from "./idle-reconciliation.js";
10
10
  import { LinearSessionSync } from "./linear-session-sync.js";
11
+ import { recoverLinearAgentActivityContext } from "./linear-agent-activity-recovery.js";
11
12
  import { IssueSessionLeaseService } from "./issue-session-lease-service.js";
12
13
  import { InterruptedRunRecovery } from "./interrupted-run-recovery.js";
13
14
  import { RunCompletionPolicy } from "./run-completion-policy.js";
@@ -234,12 +235,23 @@ export class RunOrchestrator {
234
235
  const baseContext = isRequestedChangesRunType(runType)
235
236
  ? await this.runCompletionPolicy.resolveRequestedChangesWakeContext(issue, runType, context)
236
237
  : context;
238
+ const recoveredLinearActivityContext = await recoverLinearAgentActivityContext({
239
+ linearProvider: this.linearProvider,
240
+ projectId: issue.projectId,
241
+ agentSessionId: issue.agentSessionId,
242
+ context: baseContext,
243
+ issueKey: issue.issueKey,
244
+ logger: this.logger,
245
+ });
246
+ const baseContextWithRecoveredActivity = recoveredLinearActivityContext
247
+ ? { ...baseContext, ...recoveredLinearActivityContext }
248
+ : baseContext;
237
249
  const coordinationContext = runType === "implementation"
238
250
  ? this.buildRelatedIssueContext(issue)
239
251
  : undefined;
240
252
  const effectiveContext = coordinationContext
241
- ? { ...coordinationContext, ...(baseContext ?? {}) }
242
- : baseContext;
253
+ ? { ...coordinationContext, ...baseContextWithRecoveredActivity }
254
+ : baseContextWithRecoveredActivity;
243
255
  const sourceHeadSha = typeof effectiveContext?.failureHeadSha === "string"
244
256
  ? effectiveContext.failureHeadSha
245
257
  : typeof effectiveContext?.headSha === "string"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.56.1",
3
+ "version": "0.57.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {