patchrelay 0.56.0 → 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.
- package/dist/build-info.json +3 -3
- package/dist/followup-intent.js +56 -0
- package/dist/linear-agent-activity-recovery.js +83 -0
- package/dist/linear-client.js +68 -0
- package/dist/linear-session-reporting.js +21 -0
- package/dist/prompting/patchrelay.js +6 -0
- package/dist/run-orchestrator.js +14 -2
- package/dist/webhooks/agent-session-handler.js +48 -2
- package/dist/webhooks/comment-policy.js +2 -0
- package/dist/webhooks/comment-wake-handler.js +93 -1
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const IMPLEMENTATION_PATTERNS = [
|
|
2
|
+
/\b(add|address|adjust|change|create|delete|deploy|fix|implement|install|merge|open|polish|publish|push|remove|rename|set|ship|update)\b/i,
|
|
3
|
+
/\b(go on and|go ahead and|please|pls)\s+\b(add|address|adjust|change|create|delete|deploy|fix|implement|install|merge|open|polish|publish|push|remove|rename|set|ship|update|use)\b/i,
|
|
4
|
+
/\b(use|keep|switch|move)\b.+\b(instead|copy|api|contract|behavior|state|repo|branch|team|issue|pr)\b/i,
|
|
5
|
+
];
|
|
6
|
+
const RETRY_PATTERNS = [
|
|
7
|
+
/\b(continue|go on|keep going|proceed|resume)\b/i,
|
|
8
|
+
/\b(retry|try again|rerun|run again|restart)\b/i,
|
|
9
|
+
/\b(next task|next issue)\b/i,
|
|
10
|
+
];
|
|
11
|
+
const STOP_PATTERNS = [
|
|
12
|
+
/\b(stop|cancel|halt|abort)\b/i,
|
|
13
|
+
/\b(pause|hold)\s+(work|implementation|the run|patchrelay)\b/i,
|
|
14
|
+
/\bdo not\s+(continue|proceed|work|implement)\b/i,
|
|
15
|
+
];
|
|
16
|
+
const STATUS_PATTERNS = [
|
|
17
|
+
/\b(status|progress)\b/i,
|
|
18
|
+
/\b(status update|progress update|any update|quick update)\b/i,
|
|
19
|
+
/\b(where are we|what'?s happening|what is happening|what'?s deployed|what is deployed)\b/i,
|
|
20
|
+
/\b(done so far|deployed so far|current work|current run)\b/i,
|
|
21
|
+
];
|
|
22
|
+
const CLARIFICATION_PATTERNS = [
|
|
23
|
+
/\b(fyi|for context|heads up|to clarify|clarification|correction|actually|note that)\b/i,
|
|
24
|
+
/\b(i meant|what i meant|not asking you to|no action needed)\b/i,
|
|
25
|
+
];
|
|
26
|
+
const QUESTION_PATTERNS = [
|
|
27
|
+
/\?$/,
|
|
28
|
+
/\b(can|could|do|does|did|is|are|was|were|why|how|what|when|where|which|who|should|would)\b/i,
|
|
29
|
+
];
|
|
30
|
+
export function classifyFollowupIntent(input) {
|
|
31
|
+
const text = input.trim();
|
|
32
|
+
if (!text)
|
|
33
|
+
return "clarification";
|
|
34
|
+
if (matchesAny(text, STOP_PATTERNS))
|
|
35
|
+
return "stop";
|
|
36
|
+
if (matchesAny(text, RETRY_PATTERNS))
|
|
37
|
+
return "retry";
|
|
38
|
+
if (matchesAny(text, STATUS_PATTERNS))
|
|
39
|
+
return "status";
|
|
40
|
+
if (matchesAny(text, IMPLEMENTATION_PATTERNS))
|
|
41
|
+
return "implementation_request";
|
|
42
|
+
if (matchesAny(text, CLARIFICATION_PATTERNS))
|
|
43
|
+
return "clarification";
|
|
44
|
+
if (matchesAny(text, QUESTION_PATTERNS))
|
|
45
|
+
return "question";
|
|
46
|
+
return "clarification";
|
|
47
|
+
}
|
|
48
|
+
export function followupIntentQueuesWork(intent) {
|
|
49
|
+
return intent === "implementation_request" || intent === "retry";
|
|
50
|
+
}
|
|
51
|
+
export function followupIntentIsNonActionable(intent) {
|
|
52
|
+
return intent === "status" || intent === "question" || intent === "clarification";
|
|
53
|
+
}
|
|
54
|
+
function matchesAny(text, patterns) {
|
|
55
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
56
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/linear-client.js
CHANGED
|
@@ -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;
|
|
@@ -64,6 +64,24 @@ export function buildPromptDeliveredThought(runType) {
|
|
|
64
64
|
body: `PatchRelay routed your latest instructions into the active ${lowerRunTypeLabel(runType)} workflow.`,
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
+
export function buildFollowupStatusActivity(params) {
|
|
68
|
+
const subject = params.issue.issueKey ? `${params.issue.issueKey}` : "this issue";
|
|
69
|
+
const runNote = params.activeRunType
|
|
70
|
+
? ` Active workflow: ${lowerRunTypeLabel(params.activeRunType)}.`
|
|
71
|
+
: params.pendingRunType ? ` Queued workflow: ${lowerRunTypeLabel(params.pendingRunType)}.` : "";
|
|
72
|
+
const prNote = params.issue.prNumber ? ` PR #${params.issue.prNumber}.` : "";
|
|
73
|
+
const statusNote = params.statusNote ? ` ${params.statusNote}` : "";
|
|
74
|
+
return {
|
|
75
|
+
type: "response",
|
|
76
|
+
body: `PatchRelay status: ${subject} is ${formatFactoryState(params.issue.factoryState)}.${prNote}${runNote}${statusNote}`.trim(),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function buildNonActionableFollowupActivity(intent) {
|
|
80
|
+
const body = intent === "status"
|
|
81
|
+
? "PatchRelay status is available in the current agent session."
|
|
82
|
+
: "PatchRelay did not start implementation because this looks like a question or clarification. Ask PatchRelay to continue, retry, or implement when you want work to run.";
|
|
83
|
+
return { type: "response", body };
|
|
84
|
+
}
|
|
67
85
|
export function buildRunStartedActivity(runType) {
|
|
68
86
|
switch (runType) {
|
|
69
87
|
case "review_fix":
|
|
@@ -79,6 +97,9 @@ export function buildRunStartedActivity(runType) {
|
|
|
79
97
|
return { type: "action", action: "Implementing", parameter: "requested change" };
|
|
80
98
|
}
|
|
81
99
|
}
|
|
100
|
+
function formatFactoryState(state) {
|
|
101
|
+
return state.replaceAll("_", " ");
|
|
102
|
+
}
|
|
82
103
|
export function buildRunCompletedActivity(params) {
|
|
83
104
|
const prLabel = params.prNumber ? `PR #${params.prNumber}` : "the pull request";
|
|
84
105
|
const summary = trimSummary(params.completionSummary);
|
|
@@ -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) {
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -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, ...
|
|
242
|
-
:
|
|
253
|
+
? { ...coordinationContext, ...baseContextWithRecoveredActivity }
|
|
254
|
+
: baseContextWithRecoveredActivity;
|
|
243
255
|
const sourceHeadSha = typeof effectiveContext?.failureHeadSha === "string"
|
|
244
256
|
? effectiveContext.failureHeadSha
|
|
245
257
|
: typeof effectiveContext?.headSha === "string"
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { buildAgentSessionPlanForIssue, } from "../agent-session-plan.js";
|
|
2
2
|
import { buildAgentSessionExternalUrls } from "../agent-session-presentation.js";
|
|
3
|
-
import {
|
|
3
|
+
import { classifyFollowupIntent, followupIntentIsNonActionable } from "../followup-intent.js";
|
|
4
|
+
import { extractLatestAssistantSummary } from "../issue-session-events.js";
|
|
5
|
+
import { buildAlreadyRunningThought, buildAgentSessionAcknowledgementThought, buildBlockedDelegationActivity, buildDelegationThought, buildFollowupStatusActivity, buildNonActionableFollowupActivity, buildPromptDeliveredThought, buildStopConfirmationActivity, } from "../linear-session-reporting.js";
|
|
4
6
|
import { resolveProject, triggerEventAllowed } from "../project-resolution.js";
|
|
7
|
+
import { deriveIssueStatusNote } from "../status-note.js";
|
|
5
8
|
const PATCHRELAY_AGENT_ACTIVITY_TYPES = new Set([
|
|
6
9
|
"action",
|
|
7
10
|
"elicitation",
|
|
@@ -122,6 +125,28 @@ export class AgentSessionHandler {
|
|
|
122
125
|
return;
|
|
123
126
|
}
|
|
124
127
|
const promptBody = normalized.agentSession.promptBody?.trim();
|
|
128
|
+
const directReply = promptBody && existingIssue ? params.isDirectReplyToOutstandingQuestion(existingIssue) : false;
|
|
129
|
+
const promptIntent = promptBody ? classifyFollowupIntent(promptBody) : undefined;
|
|
130
|
+
if (promptBody && existingIssue && promptIntent === "stop") {
|
|
131
|
+
await this.handleStopSignal({
|
|
132
|
+
normalized,
|
|
133
|
+
project,
|
|
134
|
+
trackedIssue,
|
|
135
|
+
activeRun,
|
|
136
|
+
linear,
|
|
137
|
+
syncAgentSession: (agentSessionId, issue, options) => this.syncAgentSession(linear, agentSessionId, issue, params.peekPendingSessionWakeRunType, options),
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (promptBody && existingIssue && promptIntent === "status" && !directReply) {
|
|
142
|
+
await this.publishAgentActivity(linear, normalized.agentSession.id, this.buildStatusActivity(existingIssue, activeRun, params.peekPendingSessionWakeRunType));
|
|
143
|
+
await this.syncAgentSession(linear, normalized.agentSession.id, existingIssue ?? trackedIssue, params.peekPendingSessionWakeRunType, activeRun ? { activeRunType: activeRun.runType } : undefined);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (promptBody && promptIntent && followupIntentIsNonActionable(promptIntent) && !directReply) {
|
|
147
|
+
await this.publishAgentActivity(linear, normalized.agentSession.id, buildNonActionableFollowupActivity(promptIntent));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
125
150
|
if (!automationEnabled && promptBody && existingIssue) {
|
|
126
151
|
await this.publishAgentActivity(linear, normalized.agentSession.id, {
|
|
127
152
|
type: "thought",
|
|
@@ -160,7 +185,10 @@ export class AgentSessionHandler {
|
|
|
160
185
|
}
|
|
161
186
|
if (promptBody && existingIssue && automationEnabled) {
|
|
162
187
|
const hadPendingWake = this.db.issueSessions.peekIssueSessionWake(project.id, normalized.issue.id) !== undefined;
|
|
163
|
-
|
|
188
|
+
if (!directReply && promptIntent && followupIntentIsNonActionable(promptIntent)) {
|
|
189
|
+
await this.publishAgentActivity(linear, normalized.agentSession.id, buildNonActionableFollowupActivity(promptIntent));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
164
192
|
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(project.id, normalized.issue.id, {
|
|
165
193
|
projectId: project.id,
|
|
166
194
|
linearIssueId: normalized.issue.id,
|
|
@@ -184,6 +212,24 @@ export class AgentSessionHandler {
|
|
|
184
212
|
await this.publishAgentActivity(linear, normalized.agentSession.id, buildDelegationThought(wakeRunType, "prompt"), { ephemeral: true });
|
|
185
213
|
}
|
|
186
214
|
}
|
|
215
|
+
buildStatusActivity(issue, activeRun, peekPendingSessionWakeRunType) {
|
|
216
|
+
const latestRun = activeRun ?? this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
217
|
+
const latestEvent = this.db.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId).at(-1);
|
|
218
|
+
const statusNote = deriveIssueStatusNote({
|
|
219
|
+
issue,
|
|
220
|
+
latestRun,
|
|
221
|
+
latestEvent,
|
|
222
|
+
sessionSummary: extractLatestAssistantSummary(latestRun),
|
|
223
|
+
waitingReason: undefined,
|
|
224
|
+
});
|
|
225
|
+
const pendingRunType = peekPendingSessionWakeRunType(issue.projectId, issue.linearIssueId);
|
|
226
|
+
return buildFollowupStatusActivity({
|
|
227
|
+
issue,
|
|
228
|
+
...(statusNote ? { statusNote } : {}),
|
|
229
|
+
...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}),
|
|
230
|
+
...(pendingRunType ? { pendingRunType } : {}),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
187
233
|
async handleStopSignal(params) {
|
|
188
234
|
const issueId = params.normalized.issue.id;
|
|
189
235
|
const sessionId = params.normalized.agentSession.id;
|
|
@@ -31,6 +31,8 @@ export function isPatchRelayGeneratedActivityComment(body) {
|
|
|
31
31
|
|| body.startsWith("PatchRelay is already working on ")
|
|
32
32
|
|| body.startsWith("PatchRelay received the ")
|
|
33
33
|
|| body.startsWith("PatchRelay routed your latest instructions into ")
|
|
34
|
+
|| body.startsWith("PatchRelay status:")
|
|
35
|
+
|| body.startsWith("PatchRelay did not start implementation ")
|
|
34
36
|
|| body.startsWith("PatchRelay has stopped work as requested.")
|
|
35
37
|
|| body.startsWith("Merge preparation failed ")
|
|
36
38
|
|| body === "This thread is for an agent session with patchrelay.";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyFollowupIntent, followupIntentIsNonActionable, followupIntentQueuesWork } from "../followup-intent.js";
|
|
1
2
|
import { triggerEventAllowed } from "../project-resolution.js";
|
|
2
3
|
import { hasExplicitPatchRelayWakeIntent, isInertPatchRelayComment, isPatchRelayManagedCommentAuthor, } from "./comment-policy.js";
|
|
3
4
|
import { classifyIssue } from "../issue-class.js";
|
|
@@ -57,9 +58,10 @@ export class CommentWakeHandler {
|
|
|
57
58
|
});
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
61
|
+
const directReply = params.isDirectReplyToOutstandingQuestion(issue);
|
|
62
|
+
const intent = classifyFollowupIntent(trimmedBody);
|
|
60
63
|
if (!issue.activeRunId) {
|
|
61
64
|
if (ENQUEUEABLE_STATES.has(issue.factoryState)) {
|
|
62
|
-
const directReply = params.isDirectReplyToOutstandingQuestion(issue);
|
|
63
65
|
const wakeIntent = issueClass === "orchestration" || directReply || hasExplicitPatchRelayWakeIntent(trimmedBody);
|
|
64
66
|
if (!wakeIntent) {
|
|
65
67
|
this.feed?.publish({
|
|
@@ -73,6 +75,42 @@ export class CommentWakeHandler {
|
|
|
73
75
|
});
|
|
74
76
|
return;
|
|
75
77
|
}
|
|
78
|
+
if (intent === "stop") {
|
|
79
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(project.id, normalized.issue.id, {
|
|
80
|
+
projectId: project.id,
|
|
81
|
+
linearIssueId: normalized.issue.id,
|
|
82
|
+
eventType: "stop_requested",
|
|
83
|
+
eventJson: JSON.stringify({
|
|
84
|
+
body: trimmedBody,
|
|
85
|
+
author: normalized.comment.userName,
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(project.id, normalized.issue.id);
|
|
89
|
+
this.feed?.publish({
|
|
90
|
+
level: "info",
|
|
91
|
+
kind: "comment",
|
|
92
|
+
projectId: project.id,
|
|
93
|
+
issueKey: trackedIssue?.issueKey,
|
|
94
|
+
status: "stopped",
|
|
95
|
+
summary: "Stop request recorded from Linear comment",
|
|
96
|
+
detail: trimmedBody.slice(0, 200),
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!directReply && !followupIntentQueuesWork(intent)) {
|
|
101
|
+
this.feed?.publish({
|
|
102
|
+
level: "info",
|
|
103
|
+
kind: "comment",
|
|
104
|
+
projectId: project.id,
|
|
105
|
+
issueKey: trackedIssue?.issueKey,
|
|
106
|
+
status: intent === "status" ? "status_requested" : "ignored",
|
|
107
|
+
summary: intent === "status"
|
|
108
|
+
? "Ignored status comment without queueing work"
|
|
109
|
+
: "Ignored non-actionable follow-up comment",
|
|
110
|
+
detail: trimmedBody.slice(0, 200),
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
76
114
|
const runType = issue.prReviewState === "changes_requested" ? "review_fix" : "implementation";
|
|
77
115
|
const hadPendingWake = this.db.issueSessions.peekIssueSessionWake(project.id, normalized.issue.id) !== undefined;
|
|
78
116
|
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(project.id, normalized.issue.id, {
|
|
@@ -102,6 +140,60 @@ export class CommentWakeHandler {
|
|
|
102
140
|
const run = this.db.runs.getRunById(issue.activeRunId);
|
|
103
141
|
if (!run?.threadId || !run.turnId)
|
|
104
142
|
return;
|
|
143
|
+
if (intent === "stop") {
|
|
144
|
+
try {
|
|
145
|
+
await this.codex.steerTurn({
|
|
146
|
+
threadId: run.threadId,
|
|
147
|
+
turnId: run.turnId,
|
|
148
|
+
input: "STOP: The user has requested you stop working immediately. Do not make further changes. Wrap up and exit.",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
this.logger.warn({ issueKey: trackedIssue?.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to steer Codex turn for comment stop request");
|
|
153
|
+
}
|
|
154
|
+
this.db.runs.finishRun(run.id, { status: "released", threadId: run.threadId, turnId: run.turnId });
|
|
155
|
+
this.db.issueSessions.upsertIssueRespectingActiveLease(project.id, normalized.issue.id, {
|
|
156
|
+
projectId: project.id,
|
|
157
|
+
linearIssueId: normalized.issue.id,
|
|
158
|
+
activeRunId: null,
|
|
159
|
+
factoryState: "awaiting_input",
|
|
160
|
+
});
|
|
161
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(project.id, normalized.issue.id, {
|
|
162
|
+
projectId: project.id,
|
|
163
|
+
linearIssueId: normalized.issue.id,
|
|
164
|
+
eventType: "stop_requested",
|
|
165
|
+
eventJson: JSON.stringify({
|
|
166
|
+
body: trimmedBody,
|
|
167
|
+
author: normalized.comment.userName,
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(project.id, normalized.issue.id);
|
|
171
|
+
this.feed?.publish({
|
|
172
|
+
level: "info",
|
|
173
|
+
kind: "comment",
|
|
174
|
+
projectId: project.id,
|
|
175
|
+
issueKey: trackedIssue?.issueKey,
|
|
176
|
+
stage: run.runType,
|
|
177
|
+
status: "stopped",
|
|
178
|
+
summary: "Stop request delivered to active workflow",
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (!directReply && followupIntentIsNonActionable(intent)) {
|
|
183
|
+
this.feed?.publish({
|
|
184
|
+
level: "info",
|
|
185
|
+
kind: "comment",
|
|
186
|
+
projectId: project.id,
|
|
187
|
+
issueKey: trackedIssue?.issueKey,
|
|
188
|
+
stage: run.runType,
|
|
189
|
+
status: intent === "status" ? "status_requested" : "ignored",
|
|
190
|
+
summary: intent === "status"
|
|
191
|
+
? "Ignored status comment without steering active workflow"
|
|
192
|
+
: "Ignored non-actionable follow-up comment",
|
|
193
|
+
detail: trimmedBody.slice(0, 200),
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
105
197
|
const body = [
|
|
106
198
|
"New Linear comment received while you are working.",
|
|
107
199
|
normalized.comment.userName ? `Author: ${normalized.comment.userName}` : undefined,
|