patchrelay 0.68.6 → 0.69.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.68.6",
4
- "commit": "b47d248df282",
5
- "builtAt": "2026-05-19T20:34:54.989Z"
3
+ "version": "0.69.0",
4
+ "commit": "e4b7cc05aa47",
5
+ "builtAt": "2026-05-22T11:30:36.298Z"
6
6
  }
package/dist/cli/data.js CHANGED
@@ -220,6 +220,7 @@ export class CliDataAccess extends CliOperatorApiClient {
220
220
  prState: dbIssue.prState,
221
221
  prReviewState: dbIssue.prReviewState,
222
222
  prCheckStatus: dbIssue.prCheckStatus,
223
+ factoryState: dbIssue.factoryState,
223
224
  pendingRunType: dbIssue.pendingRunType,
224
225
  lastRunType: issueSession?.lastRunType,
225
226
  lastGitHubFailureSource: issue.latestFailureSource,
@@ -24,6 +24,13 @@ const ISSUE_TRIAGE_DEVELOPER_INSTRUCTIONS = [
24
24
  "Use only the facts in the current prompt.",
25
25
  "Return only the requested JSON object.",
26
26
  ].join("\n");
27
+ const FOLLOWUP_INTENT_DEVELOPER_INSTRUCTIONS = [
28
+ "You are PatchRelay's follow-up intent classifier.",
29
+ "This is a read-only routing step used only to classify one human Linear follow-up.",
30
+ "Do not run commands, do not call tools, do not edit files, and do not inspect or modify the repository.",
31
+ "Use only the text and state facts in the current prompt.",
32
+ "Return only the requested JSON object.",
33
+ ].join("\n");
27
34
  export function resolveCodexAppServerLaunch(config) {
28
35
  if (!config.sourceBashrc) {
29
36
  return {
@@ -121,6 +128,17 @@ export class CodexAppServerClient extends EventEmitter {
121
128
  developerInstructions: ISSUE_TRIAGE_DEVELOPER_INSTRUCTIONS,
122
129
  });
123
130
  }
131
+ async startThreadForFollowupIntent() {
132
+ return await this.startThreadWithOverrides({ cwd: tmpdir() }, {
133
+ approvalPolicy: "never",
134
+ sandboxMode: "read-only",
135
+ model: this.config.triageModel ?? "gpt-5.4-mini",
136
+ modelProvider: this.config.triageModelProvider ?? this.config.modelProvider ?? null,
137
+ reasoningEffort: "low",
138
+ baseInstructions: null,
139
+ developerInstructions: FOLLOWUP_INTENT_DEVELOPER_INSTRUCTIONS,
140
+ });
141
+ }
124
142
  async startThreadWithOverrides(options, overrides) {
125
143
  const params = {
126
144
  cwd: options.cwd,
@@ -0,0 +1,270 @@
1
+ import { buildFollowupStatusActivity, buildNonActionableFollowupActivity, buildPromptDeliveryFailedActivity, buildPromptDeliveredThought, } from "./linear-session-reporting.js";
2
+ import { deriveIssueStatusNote } from "./status-note.js";
3
+ import { extractLatestAssistantSummary } from "./issue-session-events.js";
4
+ export class CodexConversationAdapter {
5
+ db;
6
+ codex;
7
+ wakeDispatcher;
8
+ logger;
9
+ feed;
10
+ followupClassifier;
11
+ constructor(db, codex, wakeDispatcher, logger, feed, followupClassifier) {
12
+ this.db = db;
13
+ this.codex = codex;
14
+ this.wakeDispatcher = wakeDispatcher;
15
+ this.logger = logger;
16
+ this.feed = feed;
17
+ this.followupClassifier = followupClassifier;
18
+ }
19
+ async deliverAgentInput(params) {
20
+ const body = params.body.trim();
21
+ if (!body)
22
+ return { status: "ignored" };
23
+ const issue = this.db.issues.getIssue(params.issue.projectId, params.issue.linearIssueId) ?? params.issue;
24
+ const activeRun = issue.activeRunId ? this.db.runs.getRunById(issue.activeRunId) : undefined;
25
+ const intent = await this.classify(body, params.source, issue, activeRun, params.directReply === true);
26
+ if (intent?.intent === "status" && !params.directReply) {
27
+ await params.emitActivity?.(this.buildStatusActivity(issue, activeRun, params.peekPendingSessionWakeRunType, activeRun ? "thought" : "response"), activeRun ? { ephemeral: true } : undefined);
28
+ return { status: "answered", ...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}) };
29
+ }
30
+ if (intent?.intent === "stop") {
31
+ if (activeRun) {
32
+ await this.stopActiveRun(issue, activeRun, body, params.source);
33
+ }
34
+ else {
35
+ this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
36
+ eventType: "stop_requested",
37
+ eventJson: JSON.stringify({ body, source: params.source, ...(params.author ? { author: params.author } : {}) }),
38
+ });
39
+ this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
40
+ }
41
+ return { status: "stopped", ...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}) };
42
+ }
43
+ if (!issue.delegatedToPatchRelay) {
44
+ await params.emitActivity?.({ type: "thought", body: "PatchRelay is paused because the issue is undelegated." }, { ephemeral: true });
45
+ return { status: "ignored" };
46
+ }
47
+ if (activeRun) {
48
+ return await this.steerActiveRun({
49
+ issue,
50
+ activeRun,
51
+ body,
52
+ source: params.source,
53
+ author: params.author,
54
+ emitActivity: params.emitActivity,
55
+ });
56
+ }
57
+ return await this.queueIdleInput({
58
+ project: params.project,
59
+ issue,
60
+ body,
61
+ source: params.source,
62
+ author: params.author,
63
+ directReply: params.directReply === true,
64
+ emitActivity: params.emitActivity,
65
+ });
66
+ }
67
+ async classify(body, source, issue, activeRun, directReply) {
68
+ if (!this.followupClassifier)
69
+ return undefined;
70
+ return await this.followupClassifier.classify(body, {
71
+ source: source === "agent_session_prompt" ? "agentPrompted" : "comment",
72
+ ...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}),
73
+ factoryState: issue.factoryState,
74
+ directReply,
75
+ delegatedToPatchRelay: issue.delegatedToPatchRelay,
76
+ prReviewState: issue.prReviewState,
77
+ explicitWakeIntent: true,
78
+ });
79
+ }
80
+ async steerActiveRun(params) {
81
+ const { issue, activeRun, body, source } = params;
82
+ if (!activeRun.threadId || !activeRun.turnId) {
83
+ const queuedRunType = this.queueFollowUpEvent(issue, body, source, params.author, false);
84
+ return { status: "queued", ...(queuedRunType ? { queuedRunType } : {}) };
85
+ }
86
+ const input = [
87
+ source === "agent_session_prompt"
88
+ ? "New Linear agent-session prompt received while you are working."
89
+ : "New explicitly addressed Linear issue comment received while you are working.",
90
+ params.author ? `Author: ${params.author}` : undefined,
91
+ "",
92
+ "Checkpoint contract: incorporate this instruction before your next meaningful side effect when possible. If you are already inside a non-interruptible command, finish that command, then re-plan with this input before continuing.",
93
+ "",
94
+ body,
95
+ ].filter(Boolean).join("\n");
96
+ try {
97
+ await this.codex.steerTurn({ threadId: activeRun.threadId, turnId: activeRun.turnId, input });
98
+ this.recordPromptDelivery({
99
+ issue,
100
+ source,
101
+ runId: activeRun.id,
102
+ runType: activeRun.runType,
103
+ status: "delivered",
104
+ body,
105
+ primitive: "turn/steer",
106
+ threadId: activeRun.threadId,
107
+ turnId: activeRun.turnId,
108
+ });
109
+ this.feed?.publish({
110
+ level: "info",
111
+ kind: source === "agent_session_prompt" ? "agent" : "comment",
112
+ projectId: issue.projectId,
113
+ issueKey: issue.issueKey,
114
+ stage: activeRun.runType,
115
+ status: "delivered",
116
+ summary: `Delivered agent input to active ${activeRun.runType} workflow`,
117
+ });
118
+ await params.emitActivity?.(buildPromptDeliveredThought(activeRun.runType), { ephemeral: true });
119
+ return { status: "steered", activeRunType: activeRun.runType };
120
+ }
121
+ catch (error) {
122
+ const message = error instanceof Error ? error.message : String(error);
123
+ this.logger.warn({ issueKey: issue.issueKey, error: message }, "Failed to deliver agent input to active Codex turn");
124
+ this.recordPromptDelivery({
125
+ issue,
126
+ source,
127
+ runId: activeRun.id,
128
+ runType: activeRun.runType,
129
+ status: "delivery_failed",
130
+ body,
131
+ primitive: "turn/steer",
132
+ threadId: activeRun.threadId,
133
+ turnId: activeRun.turnId,
134
+ error: message,
135
+ });
136
+ const queuedRunType = this.queueFollowUpEvent(issue, body, source, params.author, false);
137
+ this.feed?.publish({
138
+ level: "warn",
139
+ kind: source === "agent_session_prompt" ? "agent" : "comment",
140
+ projectId: issue.projectId,
141
+ issueKey: issue.issueKey,
142
+ stage: activeRun.runType,
143
+ status: "delivery_failed",
144
+ summary: `Could not deliver agent input to active ${activeRun.runType} workflow`,
145
+ });
146
+ await params.emitActivity?.(buildPromptDeliveryFailedActivity(activeRun.runType, message));
147
+ return { status: "delivery_failed", activeRunType: activeRun.runType, ...(queuedRunType ? { queuedRunType } : {}) };
148
+ }
149
+ }
150
+ async queueIdleInput(params) {
151
+ const originalIssue = params.issue;
152
+ let issue = originalIssue;
153
+ const replacementPrRequired = originalIssue.factoryState === "done" && originalIssue.prNumber !== undefined;
154
+ if (replacementPrRequired) {
155
+ issue = this.prepareReplacementWork(params.project, originalIssue);
156
+ }
157
+ const queuedRunType = this.queueFollowUpEvent(issue, params.body, params.source, params.author, params.directReply, replacementPrRequired ? originalIssue : undefined);
158
+ if (queuedRunType) {
159
+ await params.emitActivity?.(replacementPrRequired
160
+ ? { type: "action", action: "Reopening", parameter: `completed PR #${originalIssue.prNumber} for replacement work` }
161
+ : buildPromptDeliveredThought(queuedRunType), { ephemeral: true });
162
+ }
163
+ else {
164
+ await params.emitActivity?.(buildNonActionableFollowupActivity("unknown_needs_ack"));
165
+ }
166
+ return { status: queuedRunType ? "queued" : "ignored", ...(queuedRunType ? { queuedRunType } : {}) };
167
+ }
168
+ queueFollowUpEvent(issue, body, source, author, directReply, previousIssue) {
169
+ return this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
170
+ eventType: directReply ? "direct_reply" : source === "agent_session_prompt" ? "followup_prompt" : "followup_comment",
171
+ eventJson: JSON.stringify({
172
+ ...(source === "agent_session_prompt" ? { text: body } : { body }),
173
+ source: source === "agent_session_prompt" ? "linear_agent_prompt" : "linear_comment",
174
+ ...(author ? { author } : {}),
175
+ ...(previousIssue?.prNumber !== undefined
176
+ ? {
177
+ replacementPrRequired: true,
178
+ previousPrNumber: previousIssue.prNumber,
179
+ ...(previousIssue.prUrl ? { previousPrUrl: previousIssue.prUrl } : {}),
180
+ ...(previousIssue.prState ? { previousPrState: previousIssue.prState } : {}),
181
+ ...(previousIssue.prHeadSha ? { previousPrHeadSha: previousIssue.prHeadSha } : {}),
182
+ }
183
+ : {}),
184
+ }),
185
+ });
186
+ }
187
+ prepareReplacementWork(project, issue) {
188
+ const issueRef = (issue.issueKey ?? issue.linearIssueId).replace(/[^a-zA-Z0-9._-]+/g, "-");
189
+ const suffix = Date.now().toString(36);
190
+ return this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
191
+ projectId: issue.projectId,
192
+ linearIssueId: issue.linearIssueId,
193
+ factoryState: "delegated",
194
+ branchName: `${project.branchPrefix}/${issueRef}-replacement-${suffix}`,
195
+ prNumber: null,
196
+ prUrl: null,
197
+ prState: null,
198
+ prIsDraft: null,
199
+ prHeadSha: null,
200
+ prAuthorLogin: null,
201
+ prReviewState: null,
202
+ prCheckStatus: null,
203
+ lastBlockingReviewHeadSha: null,
204
+ }) ?? issue;
205
+ }
206
+ async stopActiveRun(issue, run, body, source) {
207
+ if (run.threadId && run.turnId) {
208
+ try {
209
+ await this.codex.steerTurn({
210
+ threadId: run.threadId,
211
+ turnId: run.turnId,
212
+ input: "STOP: The user has requested you stop working immediately. Do not make further changes. Wrap up and exit.",
213
+ });
214
+ }
215
+ catch (error) {
216
+ this.logger.warn({ issueKey: issue.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to steer Codex turn for stop request");
217
+ }
218
+ this.db.runs.finishRun(run.id, { status: "released", threadId: run.threadId, turnId: run.turnId });
219
+ }
220
+ this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
221
+ projectId: issue.projectId,
222
+ linearIssueId: issue.linearIssueId,
223
+ activeRunId: null,
224
+ factoryState: "awaiting_input",
225
+ });
226
+ this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
227
+ eventType: "stop_requested",
228
+ eventJson: JSON.stringify({ body, source }),
229
+ });
230
+ this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
231
+ this.db.issueSessions.releaseIssueSessionLeaseRespectingActiveLease(issue.projectId, issue.linearIssueId);
232
+ }
233
+ buildStatusActivity(issue, activeRun, peekPendingSessionWakeRunType, activityType) {
234
+ const latestRun = activeRun ?? this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
235
+ const latestEvent = this.db.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId).at(-1);
236
+ const statusNote = deriveIssueStatusNote({
237
+ issue,
238
+ latestRun,
239
+ latestEvent,
240
+ sessionSummary: extractLatestAssistantSummary(latestRun),
241
+ waitingReason: undefined,
242
+ });
243
+ const pendingRunType = peekPendingSessionWakeRunType?.(issue.projectId, issue.linearIssueId);
244
+ return buildFollowupStatusActivity({
245
+ issue,
246
+ ...(statusNote ? { statusNote } : {}),
247
+ ...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}),
248
+ ...(pendingRunType ? { pendingRunType } : {}),
249
+ activityType,
250
+ });
251
+ }
252
+ recordPromptDelivery(params) {
253
+ this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.issue.projectId, params.issue.linearIssueId, {
254
+ projectId: params.issue.projectId,
255
+ linearIssueId: params.issue.linearIssueId,
256
+ eventType: "prompt_delivered",
257
+ eventJson: JSON.stringify({
258
+ source: params.source === "agent_session_prompt" ? "linear_agent_prompt" : "linear_comment",
259
+ runId: params.runId,
260
+ runType: params.runType,
261
+ status: params.status,
262
+ body: params.body,
263
+ primitive: params.primitive,
264
+ ...(params.threadId ? { threadId: params.threadId } : {}),
265
+ ...(params.turnId ? { turnId: params.turnId } : {}),
266
+ ...(params.error ? { error: params.error } : {}),
267
+ }),
268
+ });
269
+ }
270
+ }
@@ -1,56 +1,169 @@
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();
1
+ import { getThreadTurns } from "./codex-thread-utils.js";
2
+ import { isThreadMaterializingError } from "./codex-thread-errors.js";
3
+ import { extractFirstJsonObject, safeJsonParse } from "./utils.js";
4
+ const FOLLOWUP_INTENT_TIMEOUT_MS = 45_000;
5
+ const FOLLOWUP_INTENT_POLL_MS = 1_000;
6
+ export const FOLLOWUP_INTENT_MIN_CONFIDENCE = 0.55;
7
+ export class CodexFollowupIntentClassifier {
8
+ codex;
9
+ logger;
10
+ constructor(codex, logger) {
11
+ this.codex = codex;
12
+ this.logger = logger;
13
+ }
14
+ async classify(input, context) {
15
+ if (!input.trim()) {
16
+ return lowConfidenceFollowupIntent("Empty follow-up text.");
17
+ }
18
+ try {
19
+ const thread = await this.codex.startThreadForFollowupIntent();
20
+ const turn = await this.codex.startTurn({
21
+ threadId: thread.id,
22
+ ...(thread.cwd ? { cwd: thread.cwd } : {}),
23
+ input: buildFollowupIntentPrompt(input, context),
24
+ });
25
+ const completedThread = await this.waitForTurn(thread.id, turn.turnId);
26
+ const completedTurn = getThreadTurns(completedThread).find((entry) => entry.id === turn.turnId);
27
+ const latestMessage = completedTurn?.items
28
+ .filter((item) => item.type === "agentMessage")
29
+ .at(-1)?.text;
30
+ const parsed = parseFollowupIntentClassification(latestMessage);
31
+ if (!parsed) {
32
+ this.logger.warn({ threadId: thread.id, turnId: turn.turnId }, "Follow-up intent classifier returned invalid JSON");
33
+ return lowConfidenceFollowupIntent("Classifier returned invalid JSON.");
34
+ }
35
+ if (parsed.confidence < FOLLOWUP_INTENT_MIN_CONFIDENCE) {
36
+ return {
37
+ intent: "unknown_needs_ack",
38
+ confidence: parsed.confidence,
39
+ reason: `Low confidence (${parsed.confidence}): ${parsed.reason}`,
40
+ };
41
+ }
42
+ return parsed;
43
+ }
44
+ catch (error) {
45
+ this.logger.warn({ error: error instanceof Error ? error.message : String(error) }, "Follow-up intent classification failed");
46
+ return lowConfidenceFollowupIntent("Classifier unavailable.");
47
+ }
48
+ }
49
+ async waitForTurn(threadId, turnId) {
50
+ const deadline = Date.now() + FOLLOWUP_INTENT_TIMEOUT_MS;
51
+ while (Date.now() < deadline) {
52
+ let thread;
53
+ try {
54
+ thread = await this.codex.readThread(threadId, true);
55
+ }
56
+ catch (error) {
57
+ if (isThreadMaterializingError(error)) {
58
+ await new Promise((resolve) => setTimeout(resolve, FOLLOWUP_INTENT_POLL_MS));
59
+ continue;
60
+ }
61
+ throw error;
62
+ }
63
+ const turn = getThreadTurns(thread).find((entry) => entry.id === turnId);
64
+ if (turn?.status === "completed") {
65
+ return thread;
66
+ }
67
+ if (turn?.status === "failed" || turn?.status === "interrupted") {
68
+ throw new Error(`Follow-up intent turn ${turnId} ended with status ${turn.status}`);
69
+ }
70
+ await new Promise((resolve) => setTimeout(resolve, FOLLOWUP_INTENT_POLL_MS));
71
+ }
72
+ throw new Error(`Follow-up intent timed out after ${FOLLOWUP_INTENT_TIMEOUT_MS}ms`);
73
+ }
74
+ }
75
+ export function buildFollowupIntentPrompt(input, context) {
76
+ return [
77
+ "PatchRelay follow-up intent classification",
78
+ "",
79
+ "Classify one human follow-up so PatchRelay can route it through an explicit workflow state.",
80
+ "Do not solve the task, draft a reply, or start work. Infer ordinary natural-language intent from the text and state facts.",
81
+ "Return exactly one JSON object and no extra prose.",
82
+ "",
83
+ "Schema:",
84
+ "{",
85
+ ' "intent": "stop" | "status" | "resume_or_retry" | "implementation_instruction" | "answer_to_question" | "context_only" | "unknown_needs_ack",',
86
+ ' "confidence": 0.0,',
87
+ ' "reason": "one short sentence"',
88
+ "}",
89
+ "",
90
+ "Intent definitions:",
91
+ '- "stop": the user asks PatchRelay to halt or cancel active work.',
92
+ '- "status": the user asks only for current state, progress, or what happened.',
93
+ '- "resume_or_retry": the user asks PatchRelay to continue, resume, retry, or run again.',
94
+ '- "implementation_instruction": the user gives work instructions, constraints, review feedback, or asks for code-changing work.',
95
+ '- "answer_to_question": the user is answering a PatchRelay question or unblocking awaiting-input work.',
96
+ '- "context_only": the user provides background that should not start idle work by itself.',
97
+ '- "unknown_needs_ack": the text is too ambiguous to route confidently.',
98
+ "",
99
+ "Routing facts:",
100
+ `- Source: ${context.source}`,
101
+ `- Active run type: ${context.activeRunType ?? "none"}`,
102
+ `- Factory state: ${context.factoryState ?? "unknown"}`,
103
+ `- Direct reply to outstanding PatchRelay question: ${context.directReply ? "yes" : "no"}`,
104
+ `- Delegated to PatchRelay: ${context.delegatedToPatchRelay ? "yes" : "no"}`,
105
+ `- PR review state: ${context.prReviewState ?? "none"}`,
106
+ `- Explicit PatchRelay wake context: ${context.explicitWakeIntent ? "yes" : "no"}`,
107
+ "",
108
+ "Follow-up text:",
109
+ input.trim(),
110
+ ].join("\n");
111
+ }
112
+ export function parseFollowupIntentClassification(text) {
32
113
  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));
114
+ return undefined;
115
+ const json = extractFirstJsonObject(text) ?? text;
116
+ const parsed = safeJsonParse(json);
117
+ if (!parsed)
118
+ return undefined;
119
+ const intent = parsed.intent;
120
+ const confidence = parsed.confidence;
121
+ const reason = parsed.reason;
122
+ if (!isFollowupIntent(intent))
123
+ return undefined;
124
+ if (typeof confidence !== "number" || !Number.isFinite(confidence))
125
+ return undefined;
126
+ if (typeof reason !== "string" || !reason.trim())
127
+ return undefined;
128
+ return {
129
+ intent,
130
+ confidence: Math.max(0, Math.min(1, confidence)),
131
+ reason: reason.trim().slice(0, 240),
132
+ };
133
+ }
134
+ export function lowConfidenceFollowupIntent(reason) {
135
+ return {
136
+ intent: "unknown_needs_ack",
137
+ confidence: 0,
138
+ reason,
139
+ };
140
+ }
141
+ export function followupIntentQueuesWork(classification) {
142
+ const intent = typeof classification === "string" ? classification : classification.intent;
143
+ return intent === "implementation_instruction"
144
+ || intent === "resume_or_retry"
145
+ || intent === "answer_to_question";
146
+ }
147
+ export function followupIntentIsNonActionable(classification) {
148
+ const intent = typeof classification === "string" ? classification : classification.intent;
149
+ return intent === "status"
150
+ || intent === "context_only"
151
+ || intent === "unknown_needs_ack";
152
+ }
153
+ export function followupIntentShouldSteerActiveRun(classification) {
154
+ const intent = typeof classification === "string" ? classification : classification.intent;
155
+ return intent === "implementation_instruction"
156
+ || intent === "resume_or_retry"
157
+ || intent === "answer_to_question"
158
+ || intent === "context_only"
159
+ || intent === "unknown_needs_ack";
160
+ }
161
+ function isFollowupIntent(value) {
162
+ return value === "stop"
163
+ || value === "status"
164
+ || value === "resume_or_retry"
165
+ || value === "implementation_instruction"
166
+ || value === "answer_to_question"
167
+ || value === "context_only"
168
+ || value === "unknown_needs_ack";
56
169
  }
@@ -9,6 +9,7 @@ const TERMINAL_SESSION_EVENTS = new Set([
9
9
  ]);
10
10
  const NON_ACTIONABLE_SESSION_EVENTS = new Set([
11
11
  "delegation_observed",
12
+ "prompt_delivered",
12
13
  "run_released_authority",
13
14
  ]);
14
15
  const RUN_TYPES = new Set(["implementation", "main_repair", "review_fix", "branch_upkeep", "ci_repair", "queue_repair"]);
@@ -117,6 +118,17 @@ export function deriveSessionWakePlan(issue, events) {
117
118
  ...(typeof payload?.author === "string" ? { author: payload.author } : {}),
118
119
  });
119
120
  }
121
+ if (payload?.replacementPrRequired === true) {
122
+ context.replacementPrRequired = true;
123
+ if (typeof payload.previousPrNumber === "number")
124
+ context.previousPrNumber = payload.previousPrNumber;
125
+ if (typeof payload.previousPrUrl === "string")
126
+ context.previousPrUrl = payload.previousPrUrl;
127
+ if (typeof payload.previousPrState === "string")
128
+ context.previousPrState = payload.previousPrState;
129
+ if (typeof payload.previousPrHeadSha === "string")
130
+ context.previousPrHeadSha = payload.previousPrHeadSha;
131
+ }
120
132
  if (event.eventType === "followup_prompt"
121
133
  || event.eventType === "followup_comment"
122
134
  || event.eventType === "operator_prompt") {