patchrelay 0.36.18 → 0.37.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.
@@ -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
+ }