patchrelay 0.36.17 → 0.36.19

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.36.17",
4
- "commit": "a9609f2ce42d",
5
- "builtAt": "2026-04-10T12:32:40.306Z"
3
+ "version": "0.36.19",
4
+ "commit": "4022639921ea",
5
+ "builtAt": "2026-04-10T13:21:18.771Z"
6
6
  }
@@ -1,13 +1,13 @@
1
- import { extractStageSummary, summarizeCurrentThread } from "./run-reporting.js";
2
- import { IssueOverviewQuery, parseStageReport, } from "./issue-overview-query.js";
1
+ import { IssueOverviewQuery, } from "./issue-overview-query.js";
2
+ import { PublicAgentSessionStatusQuery } from "./public-agent-session-status-query.js";
3
3
  export class IssueQueryService {
4
- db;
5
4
  runStatusProvider;
6
5
  overviewQuery;
6
+ publicStatusQuery;
7
7
  constructor(db, codex, runStatusProvider) {
8
- this.db = db;
9
8
  this.runStatusProvider = runStatusProvider;
10
9
  this.overviewQuery = new IssueOverviewQuery(db, codex, runStatusProvider);
10
+ this.publicStatusQuery = new PublicAgentSessionStatusQuery(db, this.overviewQuery);
11
11
  }
12
12
  async getIssueOverview(issueKey) {
13
13
  return await this.overviewQuery.getIssueOverview(issueKey);
@@ -16,45 +16,6 @@ export class IssueQueryService {
16
16
  return await this.runStatusProvider.getActiveRunStatus(issueKey);
17
17
  }
18
18
  async getPublicAgentSessionStatus(issueKey) {
19
- const overview = await this.overviewQuery.getIssueOverview(issueKey);
20
- if (!overview)
21
- return undefined;
22
- const issueRecord = this.db.issues.getIssueByKey(issueKey);
23
- const latestRunReport = parseStageReport(overview.latestRun?.reportJson, overview.latestRun?.status ?? "unknown");
24
- const runs = (overview.runs ?? this.overviewQuery.buildRuns(overview.issue.projectId, overview.issue.linearIssueId)).map((run) => ({
25
- run: {
26
- id: run.id,
27
- runType: run.runType,
28
- status: run.status,
29
- startedAt: run.startedAt,
30
- ...(run.endedAt ? { endedAt: run.endedAt } : {}),
31
- },
32
- ...(run.report ? { report: run.report } : {}),
33
- }));
34
- return {
35
- issue: {
36
- issueKey: overview.issue.issueKey,
37
- title: overview.issue.title,
38
- issueUrl: overview.issue.issueUrl,
39
- currentLinearState: overview.issue.currentLinearState,
40
- ...(overview.session?.sessionState ? { sessionState: overview.session.sessionState } : {}),
41
- factoryState: overview.issue.factoryState,
42
- ...(overview.session?.prNumber !== undefined ? { prNumber: overview.session.prNumber } : {}),
43
- ...(issueRecord?.prUrl ? { prUrl: issueRecord.prUrl } : {}),
44
- ...(issueRecord?.prState ? { prState: issueRecord.prState } : {}),
45
- ...(issueRecord?.prReviewState ? { prReviewState: issueRecord.prReviewState } : {}),
46
- ...(issueRecord?.prCheckStatus ? { prCheckStatus: issueRecord.prCheckStatus } : {}),
47
- ...(issueRecord ? { ciRepairAttempts: issueRecord.ciRepairAttempts, queueRepairAttempts: issueRecord.queueRepairAttempts } : {}),
48
- ...(overview.issue.waitingReason ? { waitingReason: overview.issue.waitingReason } : {}),
49
- ...(overview.issue.statusNote ? { statusNote: overview.issue.statusNote } : {}),
50
- ...(overview.session?.lastWakeReason ? { lastWakeReason: overview.session.lastWakeReason } : {}),
51
- },
52
- ...(overview.activeRun ? { activeRun: overview.activeRun } : {}),
53
- ...(overview.latestRun ? { latestRun: overview.latestRun } : {}),
54
- ...(overview.liveThread ? { liveThread: summarizeCurrentThread(overview.liveThread) } : {}),
55
- ...(latestRunReport ? { latestReportSummary: extractStageSummary(latestRunReport) } : {}),
56
- runs,
57
- generatedAt: new Date().toISOString(),
58
- };
19
+ return await this.publicStatusQuery.getStatus(issueKey);
59
20
  }
60
21
  }
@@ -0,0 +1,109 @@
1
+ import { buildAgentSessionPlanForIssue } from "./agent-session-plan.js";
2
+ import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
3
+ export class LinearAgentSessionClient {
4
+ config;
5
+ db;
6
+ linearProvider;
7
+ logger;
8
+ feed;
9
+ constructor(config, db, linearProvider, logger, feed) {
10
+ this.config = config;
11
+ this.db = db;
12
+ this.linearProvider = linearProvider;
13
+ this.logger = logger;
14
+ this.feed = feed;
15
+ }
16
+ ensureAgentSessionIssue(issue) {
17
+ if (issue.agentSessionId) {
18
+ return issue;
19
+ }
20
+ const recoveredAgentSessionId = this.db.webhookEvents.findLatestAgentSessionIdForIssue(issue.linearIssueId);
21
+ if (!recoveredAgentSessionId)
22
+ return issue;
23
+ this.logger.info({ issueKey: issue.issueKey, agentSessionId: recoveredAgentSessionId }, "Recovered missing Linear agent session id from webhook history");
24
+ return this.db.issues.upsertIssue({
25
+ projectId: issue.projectId,
26
+ linearIssueId: issue.linearIssueId,
27
+ agentSessionId: recoveredAgentSessionId,
28
+ });
29
+ }
30
+ async emitActivity(issue, content, options) {
31
+ const syncedIssue = this.ensureAgentSessionIssue(issue);
32
+ if (!syncedIssue.agentSessionId)
33
+ return;
34
+ try {
35
+ const linear = await this.linearProvider.forProject(syncedIssue.projectId);
36
+ if (!linear)
37
+ return;
38
+ const allowEphemeral = content.type === "thought" || content.type === "action";
39
+ await linear.createAgentActivity({
40
+ agentSessionId: syncedIssue.agentSessionId,
41
+ content,
42
+ ...(options?.ephemeral && allowEphemeral ? { ephemeral: true } : {}),
43
+ });
44
+ }
45
+ catch (error) {
46
+ const msg = error instanceof Error ? error.message : String(error);
47
+ this.logger.warn({ issueKey: syncedIssue.issueKey, type: content.type, error: msg }, "Failed to emit Linear activity");
48
+ this.feed?.publish({
49
+ level: "warn",
50
+ kind: "linear",
51
+ issueKey: syncedIssue.issueKey,
52
+ projectId: syncedIssue.projectId,
53
+ status: "linear_error",
54
+ summary: `Linear activity failed: ${msg}`,
55
+ });
56
+ }
57
+ }
58
+ async syncSessionPlan(issue, linear, options) {
59
+ if (!issue.agentSessionId || !linear.updateAgentSession) {
60
+ return;
61
+ }
62
+ const externalUrls = buildAgentSessionExternalUrls(this.config, {
63
+ ...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
64
+ ...(issue.prUrl ? { prUrl: issue.prUrl } : {}),
65
+ });
66
+ await linear.updateAgentSession({
67
+ agentSessionId: issue.agentSessionId,
68
+ plan: buildAgentSessionPlanForIssue(issue, options),
69
+ ...(externalUrls ? { externalUrls } : {}),
70
+ });
71
+ }
72
+ async syncCodexPlan(issue, params) {
73
+ const syncedIssue = this.ensureAgentSessionIssue(issue);
74
+ if (!syncedIssue.agentSessionId)
75
+ return;
76
+ const plan = params.plan;
77
+ if (!Array.isArray(plan))
78
+ return;
79
+ const STATUS_MAP = {
80
+ pending: "pending",
81
+ inProgress: "inProgress",
82
+ completed: "completed",
83
+ };
84
+ const steps = plan.map((entry) => {
85
+ const e = entry;
86
+ const step = typeof e.step === "string" ? e.step : String(e.step ?? "");
87
+ const status = typeof e.status === "string" ? (STATUS_MAP[e.status] ?? "pending") : "pending";
88
+ return { content: step, status };
89
+ });
90
+ const fullPlan = [
91
+ { content: "Prepare workspace", status: "completed" },
92
+ ...steps,
93
+ { content: "Merge", status: "pending" },
94
+ ];
95
+ try {
96
+ const linear = await this.linearProvider.forProject(syncedIssue.projectId);
97
+ if (!linear?.updateAgentSession)
98
+ return;
99
+ await linear.updateAgentSession({
100
+ agentSessionId: syncedIssue.agentSessionId,
101
+ plan: fullPlan,
102
+ });
103
+ }
104
+ catch (error) {
105
+ const msg = error instanceof Error ? error.message : String(error);
106
+ this.logger.warn({ issueKey: syncedIssue.issueKey, error: msg }, "Failed to sync codex plan to Linear");
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,185 @@
1
+ import { sanitizeOperatorFacingCommand, sanitizeOperatorFacingText } from "./presentation-text.js";
2
+ const PROGRESS_THROTTLE_MS = 5_000;
3
+ const MAX_PROGRESS_TEXT_LENGTH = 220;
4
+ export class LinearProgressReporter {
5
+ db;
6
+ emitActivity;
7
+ progressThrottle = new Map();
8
+ workingOnPublishedRuns = new Set();
9
+ agentMessageBuffers = new Map();
10
+ agentMessageProgressPublished = new Set();
11
+ constructor(db, emitActivity) {
12
+ this.db = db;
13
+ this.emitActivity = emitActivity;
14
+ }
15
+ maybeEmitProgress(notification, run) {
16
+ const issue = this.db.issues.getIssue(run.projectId, run.linearIssueId);
17
+ if (!issue)
18
+ return;
19
+ const agentSentence = this.consumeAgentMessageSentence(notification, run);
20
+ const workingOn = resolveWorkingOnActivity(notification, agentSentence?.sentence);
21
+ if (workingOn && !this.workingOnPublishedRuns.has(run.id)) {
22
+ this.workingOnPublishedRuns.add(run.id);
23
+ void this.emitActivity(issue, workingOn);
24
+ }
25
+ const progress = resolveEphemeralProgressActivity(notification, agentSentence?.sentence);
26
+ if (!progress)
27
+ return;
28
+ if (!progress.bypassThrottle) {
29
+ const now = Date.now();
30
+ const lastEmit = this.progressThrottle.get(run.id) ?? 0;
31
+ if (now - lastEmit < PROGRESS_THROTTLE_MS)
32
+ return;
33
+ this.progressThrottle.set(run.id, now);
34
+ }
35
+ void this.emitActivity(issue, progress.activity, { ephemeral: true });
36
+ }
37
+ clearProgress(runId) {
38
+ this.progressThrottle.delete(runId);
39
+ this.workingOnPublishedRuns.delete(runId);
40
+ for (const key of this.agentMessageBuffers.keys()) {
41
+ if (key.startsWith(`${runId}:`)) {
42
+ this.agentMessageBuffers.delete(key);
43
+ }
44
+ }
45
+ for (const key of this.agentMessageProgressPublished) {
46
+ if (key.startsWith(`${runId}:`)) {
47
+ this.agentMessageProgressPublished.delete(key);
48
+ }
49
+ }
50
+ }
51
+ consumeAgentMessageSentence(notification, run) {
52
+ const messageKey = resolveAgentMessageKey(notification, run);
53
+ if (!messageKey)
54
+ return undefined;
55
+ if (this.agentMessageProgressPublished.has(messageKey))
56
+ return undefined;
57
+ const delta = resolveAgentMessageDelta(notification);
58
+ if (delta) {
59
+ const previous = this.agentMessageBuffers.get(messageKey) ?? "";
60
+ const next = `${previous}${delta}`;
61
+ this.agentMessageBuffers.set(messageKey, next);
62
+ const sentence = extractFirstCompletedSentence(next);
63
+ if (!sentence)
64
+ return undefined;
65
+ this.agentMessageProgressPublished.add(messageKey);
66
+ return { sentence };
67
+ }
68
+ const completedText = resolveCompletedAgentMessageText(notification);
69
+ if (!completedText)
70
+ return undefined;
71
+ const sentence = extractFirstSentence(completedText);
72
+ if (!sentence)
73
+ return undefined;
74
+ this.agentMessageProgressPublished.add(messageKey);
75
+ return { sentence };
76
+ }
77
+ }
78
+ function resolveWorkingOnActivity(notification, agentSentence) {
79
+ const summary = resolveWorkingOnSummary(notification) ?? agentSentence;
80
+ if (!summary)
81
+ return undefined;
82
+ return { type: "response", body: `Working on: ${summary}` };
83
+ }
84
+ function resolveEphemeralProgressActivity(notification, agentSentence) {
85
+ if (notification.method === "item/started") {
86
+ const item = notification.params.item;
87
+ if (!item)
88
+ return undefined;
89
+ const type = typeof item.type === "string" ? item.type : undefined;
90
+ if (type === "commandExecution") {
91
+ const cmd = item.command;
92
+ const cmdStr = Array.isArray(cmd)
93
+ ? sanitizeOperatorFacingCommand(cmd.map((part) => String(part)).join(" "))
94
+ : sanitizeOperatorFacingCommand(typeof cmd === "string" ? cmd : undefined);
95
+ return { activity: { type: "action", action: "Running", parameter: truncateProgressText(cmdStr ?? "command", 120) } };
96
+ }
97
+ if (type === "mcpToolCall") {
98
+ const server = typeof item.server === "string" ? item.server : "";
99
+ const tool = typeof item.tool === "string" ? item.tool : "";
100
+ return { activity: { type: "action", action: "Using", parameter: `${server}/${tool}` } };
101
+ }
102
+ if (type === "dynamicToolCall") {
103
+ const tool = typeof item.tool === "string" ? item.tool : "tool";
104
+ return { activity: { type: "action", action: "Using", parameter: tool } };
105
+ }
106
+ }
107
+ if (agentSentence) {
108
+ return {
109
+ activity: { type: "thought", body: agentSentence },
110
+ bypassThrottle: true,
111
+ };
112
+ }
113
+ return undefined;
114
+ }
115
+ function resolveWorkingOnSummary(notification) {
116
+ if (notification.method !== "turn/plan/updated") {
117
+ return undefined;
118
+ }
119
+ const plan = notification.params.plan;
120
+ if (!Array.isArray(plan))
121
+ return undefined;
122
+ const ranked = plan
123
+ .map((entry) => entry)
124
+ .filter((entry) => typeof entry.step === "string" && entry.step.trim().length > 0)
125
+ .sort((a, b) => rankPlanStatus(a.status) - rankPlanStatus(b.status));
126
+ const first = ranked[0];
127
+ return summarizeProgressSentence(typeof first?.step === "string" ? first.step : undefined);
128
+ }
129
+ function rankPlanStatus(status) {
130
+ return status === "inProgress" ? 0
131
+ : status === "pending" ? 1
132
+ : status === "completed" ? 2
133
+ : 3;
134
+ }
135
+ function resolveAgentMessageKey(notification, run) {
136
+ if (notification.method === "item/agentMessage/delta") {
137
+ const itemId = typeof notification.params.itemId === "string" ? notification.params.itemId : undefined;
138
+ return itemId ? `${run.id}:${itemId}` : undefined;
139
+ }
140
+ if (notification.method === "item/completed") {
141
+ const item = notification.params.item;
142
+ const itemId = typeof item?.id === "string" ? item.id : undefined;
143
+ const itemType = typeof item?.type === "string" ? item.type : undefined;
144
+ return itemId && itemType === "agentMessage" ? `${run.id}:${itemId}` : undefined;
145
+ }
146
+ return undefined;
147
+ }
148
+ function resolveAgentMessageDelta(notification) {
149
+ if (notification.method !== "item/agentMessage/delta") {
150
+ return undefined;
151
+ }
152
+ return typeof notification.params.delta === "string" ? notification.params.delta : undefined;
153
+ }
154
+ function resolveCompletedAgentMessageText(notification) {
155
+ if (notification.method !== "item/completed") {
156
+ return undefined;
157
+ }
158
+ const item = notification.params.item;
159
+ if (!item || item.type !== "agentMessage")
160
+ return undefined;
161
+ return typeof item.text === "string" ? item.text : undefined;
162
+ }
163
+ function extractFirstSentence(text) {
164
+ const sanitized = sanitizeOperatorFacingText(text)?.replace(/\s+/g, " ").trim();
165
+ if (!sanitized)
166
+ return undefined;
167
+ const match = sanitized.match(/^(.+?[.!?])(?:\s|$)/);
168
+ return truncateProgressText((match?.[1] ?? sanitized).trim(), MAX_PROGRESS_TEXT_LENGTH);
169
+ }
170
+ function extractFirstCompletedSentence(text) {
171
+ const sanitized = sanitizeOperatorFacingText(text)?.replace(/\s+/g, " ").trim();
172
+ if (!sanitized)
173
+ return undefined;
174
+ const match = sanitized.match(/^(.+?[.!?])(?:\s|$)/);
175
+ return match?.[1] ? truncateProgressText(match[1].trim(), MAX_PROGRESS_TEXT_LENGTH) : undefined;
176
+ }
177
+ function summarizeProgressSentence(text) {
178
+ const summary = extractFirstSentence(text);
179
+ if (!summary)
180
+ return undefined;
181
+ return summary.endsWith(".") || summary.endsWith("!") || summary.endsWith("?") ? summary : `${summary}.`;
182
+ }
183
+ function truncateProgressText(text, maxLength) {
184
+ return text.length <= maxLength ? text : `${text.slice(0, maxLength - 3).trimEnd()}...`;
185
+ }