patchrelay 0.78.1 → 0.79.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/agent-input-service.js +1 -1
- package/dist/build-info.json +3 -3
- package/dist/git-worktree-status.js +1 -1
- package/dist/idle-reconciliation-helpers.js +2 -4
- package/dist/idle-reconciliation.js +3 -2
- package/dist/issue-session-events.js +219 -53
- package/dist/linear-agent-activity-recovery.js +2 -8
- package/dist/operator-retry-event.js +12 -6
- package/dist/prompting/patchrelay.js +47 -80
- package/dist/queue-health-monitor.js +4 -3
- package/dist/run-context.js +382 -0
- package/dist/run-failure-policy.js +2 -1
- package/dist/run-finalizer.js +44 -28
- package/dist/run-launcher.js +3 -5
- package/dist/run-orchestrator.js +6 -8
- package/dist/run-wake-planner.js +40 -30
- package/dist/status-note.js +36 -18
- package/dist/utils.js +9 -0
- package/dist/webhooks/issue-update-plan.js +2 -1
- package/dist/workflow-wake-resolver.js +20 -10
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildFollowupStatusActivity, buildNonActionableFollowupActivity, buildPromptDeliveryFailedActivity, buildPromptDeliveredThought, } from "./linear-session-reporting.js";
|
|
2
2
|
import { deriveIssueStatusNote } from "./status-note.js";
|
|
3
|
-
import { extractLatestAssistantSummary } from "./issue-session-events.js";
|
|
3
|
+
import { extractLatestAssistantSummary, } from "./issue-session-events.js";
|
|
4
4
|
import { dirtyWorktreeEventPayload, inspectGitWorktreeStatus } from "./git-worktree-status.js";
|
|
5
5
|
const WRITER = "agent-input-service";
|
|
6
6
|
export class AgentInputService {
|
package/dist/build-info.json
CHANGED
|
@@ -64,6 +64,6 @@ export function dirtyWorktreeEventPayload(status) {
|
|
|
64
64
|
mergeInProgress: status.mergeInProgress,
|
|
65
65
|
unmergedPaths: status.unmergedPaths,
|
|
66
66
|
changedPaths: status.changedPaths,
|
|
67
|
-
summary: status.summary,
|
|
67
|
+
...(status.summary !== undefined ? { summary: status.summary } : {}),
|
|
68
68
|
};
|
|
69
69
|
}
|
|
@@ -46,10 +46,8 @@ export function getGateCheckNames(project) {
|
|
|
46
46
|
* to a stale one without the timestamp check.
|
|
47
47
|
*/
|
|
48
48
|
export function isDuplicateRepairAttempt(issue, context) {
|
|
49
|
-
const signature =
|
|
50
|
-
const headSha =
|
|
51
|
-
? context.failureHeadSha
|
|
52
|
-
: typeof context?.headSha === "string" ? context.headSha : undefined;
|
|
49
|
+
const signature = context?.failureSignature;
|
|
50
|
+
const headSha = context?.failureHeadSha ?? context?.headSha;
|
|
53
51
|
if (!signature)
|
|
54
52
|
return false;
|
|
55
53
|
if (issue.lastAttemptedFailureSignature !== signature)
|
|
@@ -6,6 +6,7 @@ import { buildBranchUpkeepContext, buildFailureContext, getGateCheckNames, hasCo
|
|
|
6
6
|
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
7
7
|
import { deriveGateCheckStatusFromRollup } from "./github-rollup.js";
|
|
8
8
|
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
9
|
+
import { serializeRunContext } from "./run-context.js";
|
|
9
10
|
import { buildClosedPrCleanupFields, resolveClosedPrDisposition } from "./pr-state.js";
|
|
10
11
|
import { getReviewFixBudget } from "./run-budgets.js";
|
|
11
12
|
import { queueSettledOrchestrationIssue } from "./orchestration-parent-wake.js";
|
|
@@ -348,13 +349,13 @@ export class IdleIssueReconciler {
|
|
|
348
349
|
this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
|
|
349
350
|
eventType,
|
|
350
351
|
...(context || requestedChangesIdentity ? {
|
|
351
|
-
eventJson:
|
|
352
|
+
eventJson: serializeRunContext({
|
|
352
353
|
...context,
|
|
353
354
|
...(requestedChangesIdentity ? {
|
|
354
355
|
requestedChangesCoalesceKey: requestedChangesIdentity.coalesceKey,
|
|
355
356
|
...(requestedChangesIdentity.headSha ? { requestedChangesHeadSha: requestedChangesIdentity.headSha } : {}),
|
|
356
357
|
} : {}),
|
|
357
|
-
}),
|
|
358
|
+
}, "reconciliation wake context"),
|
|
358
359
|
} : {}),
|
|
359
360
|
dedupeKey,
|
|
360
361
|
});
|
|
@@ -1,4 +1,171 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { sanitizeOperatorFacingText } from "./presentation-text.js";
|
|
3
|
+
import { runContextSchema } from "./run-context.js";
|
|
4
|
+
import { assertNever } from "./utils.js";
|
|
5
|
+
// ─── Typed session-event payloads (plan §D2) ──────────────────────────
|
|
6
|
+
//
|
|
7
|
+
// Each eventType gets a typed payload; `parseIssueSessionEvent` is the parse
|
|
8
|
+
// boundary over the stringly DB storage (event_json stays JSON text). The
|
|
9
|
+
// same doctrine as D1 applies: malformed payloads fail loudly at parse, and
|
|
10
|
+
// boundary callers that iterate possibly-old DB rows degrade gracefully via
|
|
11
|
+
// `parseIssueSessionEventOrWarn`. All payload schemas are loose objects:
|
|
12
|
+
// legacy rows carry fields newer code no longer writes, and wake payloads are
|
|
13
|
+
// merged wholesale into the run context (which tolerates unknown keys too).
|
|
14
|
+
/** Human input payload for direct_reply / followup_prompt / followup_comment
|
|
15
|
+
* / operator_prompt. Produced by agent-input-service.ts,
|
|
16
|
+
* github-pr-comment-handler.ts and webhooks/agent-session-handler.ts;
|
|
17
|
+
* consumed by deriveSessionWakePlan (followUps + replacement-PR facts). */
|
|
18
|
+
const inputMessagePayloadSchema = z.looseObject({
|
|
19
|
+
text: z.string().optional(),
|
|
20
|
+
body: z.string().optional(),
|
|
21
|
+
author: z.string().optional(),
|
|
22
|
+
source: z.string().optional(),
|
|
23
|
+
operatorSource: z.string().optional(),
|
|
24
|
+
replacementPrRequired: z.boolean().optional(),
|
|
25
|
+
previousPrNumber: z.number().optional(),
|
|
26
|
+
previousPrUrl: z.string().optional(),
|
|
27
|
+
previousPrState: z.string().optional(),
|
|
28
|
+
previousPrHeadSha: z.string().optional(),
|
|
29
|
+
});
|
|
30
|
+
/** Produced by agent-input-service.ts after steering a running turn;
|
|
31
|
+
* consumed by run-finalizer.ts summarizePromptDeliveryEvents. */
|
|
32
|
+
const promptDeliveredPayloadSchema = z.looseObject({
|
|
33
|
+
source: z.string().optional(),
|
|
34
|
+
runId: z.number().optional(),
|
|
35
|
+
runType: z.string().optional(),
|
|
36
|
+
status: z.enum(["delivered", "delivery_failed"]).optional(),
|
|
37
|
+
body: z.string().optional(),
|
|
38
|
+
primitive: z.string().optional(),
|
|
39
|
+
threadId: z.string().optional(),
|
|
40
|
+
turnId: z.string().optional(),
|
|
41
|
+
error: z.string().optional(),
|
|
42
|
+
});
|
|
43
|
+
/** Produced by agent-input-service.ts / service-issue-actions.ts /
|
|
44
|
+
* webhooks/agent-session-handler.ts; consumed by status-note.ts. */
|
|
45
|
+
const stopRequestedPayloadSchema = z.looseObject({
|
|
46
|
+
body: z.string().optional(),
|
|
47
|
+
source: z.string().optional(),
|
|
48
|
+
author: z.string().optional(),
|
|
49
|
+
// Dirty-worktree facts from git-worktree-status.ts dirtyWorktreeEventPayload.
|
|
50
|
+
summary: z.string().optional(),
|
|
51
|
+
dirtyWorktree: z.boolean().optional(),
|
|
52
|
+
mergeInProgress: z.boolean().optional(),
|
|
53
|
+
unmergedPaths: z.array(z.string()).optional(),
|
|
54
|
+
changedPaths: z.array(z.string()).optional(),
|
|
55
|
+
});
|
|
56
|
+
/** Produced by webhooks/desired-stage-recorder.ts; consumed by status-note.ts. */
|
|
57
|
+
const undelegatedPayloadSchema = z.looseObject({
|
|
58
|
+
// Dirty-worktree facts from git-worktree-status.ts dirtyWorktreeEventPayload.
|
|
59
|
+
summary: z.string().optional(),
|
|
60
|
+
dirtyWorktree: z.boolean().optional(),
|
|
61
|
+
mergeInProgress: z.boolean().optional(),
|
|
62
|
+
unmergedPaths: z.array(z.string()).optional(),
|
|
63
|
+
changedPaths: z.array(z.string()).optional(),
|
|
64
|
+
});
|
|
65
|
+
/** Produced by service-issue-actions.ts / cli/data.ts when an operator
|
|
66
|
+
* force-closes an issue. */
|
|
67
|
+
const operatorClosedPayloadSchema = z.looseObject({
|
|
68
|
+
terminalState: z.enum(["done", "failed"]).optional(),
|
|
69
|
+
reason: z.string().optional(),
|
|
70
|
+
});
|
|
71
|
+
/** Audit payloads (delegation-audit.ts) and marker events carry free-form
|
|
72
|
+
* diagnostic objects; nothing branches on their fields. */
|
|
73
|
+
const freeFormPayloadSchema = z.looseObject({});
|
|
74
|
+
export class IssueSessionEventPayloadError extends Error {
|
|
75
|
+
constructor(message, options) {
|
|
76
|
+
super(message, options);
|
|
77
|
+
this.name = "IssueSessionEventPayloadError";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function parsePayloadJson(event) {
|
|
81
|
+
if (!event.eventJson)
|
|
82
|
+
return undefined;
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(event.eventJson);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw new IssueSessionEventPayloadError(`Malformed ${event.eventType} session-event payload JSON: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
89
|
+
}
|
|
90
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
91
|
+
throw new IssueSessionEventPayloadError(`Malformed ${event.eventType} session-event payload: expected a JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}`);
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
function parseWithSchema(event, schema) {
|
|
96
|
+
const raw = parsePayloadJson(event);
|
|
97
|
+
if (raw === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
const result = schema.safeParse(raw);
|
|
100
|
+
if (!result.success) {
|
|
101
|
+
throw new IssueSessionEventPayloadError(`Invalid ${event.eventType} session-event payload: ${result.error.issues
|
|
102
|
+
.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
|
|
103
|
+
.join("; ")}`, { cause: result.error });
|
|
104
|
+
}
|
|
105
|
+
return result.data;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Parse boundary for session events: returns the typed union member for the
|
|
109
|
+
* event. FAILS LOUDLY (IssueSessionEventPayloadError) on malformed JSON or
|
|
110
|
+
* schema violations — boundary callers over possibly-old DB rows should use
|
|
111
|
+
* `parseIssueSessionEventOrWarn` instead.
|
|
112
|
+
*/
|
|
113
|
+
export function parseIssueSessionEvent(event) {
|
|
114
|
+
const eventType = event.eventType;
|
|
115
|
+
switch (eventType) {
|
|
116
|
+
case "delegated":
|
|
117
|
+
case "child_changed":
|
|
118
|
+
case "child_delivered":
|
|
119
|
+
case "child_regressed":
|
|
120
|
+
case "completion_check_continue":
|
|
121
|
+
case "review_changes_requested":
|
|
122
|
+
case "settled_red_ci":
|
|
123
|
+
case "merge_steward_incident":
|
|
124
|
+
return { eventType, payload: parseWithSchema(event, runContextSchema) };
|
|
125
|
+
case "direct_reply":
|
|
126
|
+
case "followup_prompt":
|
|
127
|
+
case "followup_comment":
|
|
128
|
+
case "operator_prompt":
|
|
129
|
+
return { eventType, payload: parseWithSchema(event, inputMessagePayloadSchema) };
|
|
130
|
+
case "prompt_delivered":
|
|
131
|
+
return { eventType, payload: parseWithSchema(event, promptDeliveredPayloadSchema) };
|
|
132
|
+
case "stop_requested":
|
|
133
|
+
return { eventType, payload: parseWithSchema(event, stopRequestedPayloadSchema) };
|
|
134
|
+
case "operator_closed":
|
|
135
|
+
return { eventType, payload: parseWithSchema(event, operatorClosedPayloadSchema) };
|
|
136
|
+
case "undelegated":
|
|
137
|
+
return { eventType, payload: parseWithSchema(event, undelegatedPayloadSchema) };
|
|
138
|
+
case "delegation_observed":
|
|
139
|
+
case "self_comment":
|
|
140
|
+
case "issue_removed":
|
|
141
|
+
case "pr_closed":
|
|
142
|
+
case "pr_merged":
|
|
143
|
+
case "run_released_authority":
|
|
144
|
+
return { eventType, payload: parseWithSchema(event, freeFormPayloadSchema) };
|
|
145
|
+
default:
|
|
146
|
+
// Also reached at runtime for event_type values written by versions
|
|
147
|
+
// that no longer exist in the union; the OrWarn boundary degrades them.
|
|
148
|
+
return assertNever(eventType, "Unknown issue session event type");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Boundary variant: parse loudly, but degrade a bad payload to `undefined`
|
|
153
|
+
* (and an unknown stored event type to `undefined` entirely) after reporting
|
|
154
|
+
* through `warn`. The parse itself never silently coerces.
|
|
155
|
+
*/
|
|
156
|
+
export function parseIssueSessionEventOrWarn(event, warn) {
|
|
157
|
+
try {
|
|
158
|
+
return parseIssueSessionEvent(event);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
warn?.(error instanceof Error ? error.message : String(error));
|
|
162
|
+
if (error instanceof IssueSessionEventPayloadError) {
|
|
163
|
+
// The event type itself is known — keep the event, drop the payload.
|
|
164
|
+
return { eventType: event.eventType, payload: undefined };
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
2
169
|
const TERMINAL_SESSION_EVENTS = new Set([
|
|
3
170
|
"stop_requested",
|
|
4
171
|
"operator_closed",
|
|
@@ -20,7 +187,7 @@ const RUN_TYPES = new Set(["implementation", "review_fix", "branch_upkeep", "ci_
|
|
|
20
187
|
function parseRunType(value) {
|
|
21
188
|
return typeof value === "string" && RUN_TYPES.has(value) ? value : undefined;
|
|
22
189
|
}
|
|
23
|
-
export function deriveSessionWakePlan(issue, events) {
|
|
190
|
+
export function deriveSessionWakePlan(issue, events, onPayloadError) {
|
|
24
191
|
const actionableEvents = events.filter((event) => !NON_ACTIONABLE_SESSION_EVENTS.has(event.eventType));
|
|
25
192
|
if (actionableEvents.length === 0)
|
|
26
193
|
return undefined;
|
|
@@ -34,53 +201,58 @@ export function deriveSessionWakePlan(issue, events) {
|
|
|
34
201
|
let runType;
|
|
35
202
|
let resumeThread = false;
|
|
36
203
|
for (const event of actionableEvents) {
|
|
37
|
-
|
|
38
|
-
|
|
204
|
+
// Boundary over DB rows: a payload written by an older version that no
|
|
205
|
+
// longer matches the schema degrades to "no payload" instead of wedging
|
|
206
|
+
// wake derivation for the whole issue.
|
|
207
|
+
const typed = parseIssueSessionEventOrWarn(event, onPayloadError ? (message) => onPayloadError(event, message) : undefined);
|
|
208
|
+
if (!typed)
|
|
209
|
+
continue;
|
|
210
|
+
switch (typed.eventType) {
|
|
39
211
|
case "merge_steward_incident":
|
|
40
212
|
runType = "queue_repair";
|
|
41
213
|
wakeReason = "merge_steward_incident";
|
|
42
214
|
eventIds = [event.id];
|
|
43
|
-
Object.assign(context, payload ?? {});
|
|
215
|
+
Object.assign(context, typed.payload ?? {});
|
|
44
216
|
break;
|
|
45
217
|
case "settled_red_ci":
|
|
46
218
|
if (runType !== "queue_repair") {
|
|
47
219
|
runType = "ci_repair";
|
|
48
220
|
wakeReason = "settled_red_ci";
|
|
49
221
|
eventIds = [event.id];
|
|
50
|
-
Object.assign(context, payload ?? {});
|
|
222
|
+
Object.assign(context, typed.payload ?? {});
|
|
51
223
|
}
|
|
52
224
|
break;
|
|
53
225
|
case "review_changes_requested":
|
|
54
226
|
if (runType !== "queue_repair" && runType !== "ci_repair") {
|
|
55
|
-
runType = payload?.branchUpkeepRequired === true ? "branch_upkeep" : "review_fix";
|
|
56
|
-
wakeReason = payload?.branchUpkeepRequired === true ? "branch_upkeep" : "review_changes_requested";
|
|
227
|
+
runType = typed.payload?.branchUpkeepRequired === true ? "branch_upkeep" : "review_fix";
|
|
228
|
+
wakeReason = typed.payload?.branchUpkeepRequired === true ? "branch_upkeep" : "review_changes_requested";
|
|
57
229
|
eventIds = [event.id];
|
|
58
|
-
Object.assign(context, payload ?? {});
|
|
230
|
+
Object.assign(context, typed.payload ?? {});
|
|
59
231
|
}
|
|
60
232
|
break;
|
|
61
233
|
case "delegated":
|
|
62
234
|
if (!runType) {
|
|
63
|
-
runType = parseRunType(payload?.runType) ?? "implementation";
|
|
235
|
+
runType = parseRunType(typed.payload?.runType) ?? "implementation";
|
|
64
236
|
wakeReason = issue.issueClass === "orchestration" ? "initial_delegate" : "delegated";
|
|
65
237
|
eventIds = [event.id];
|
|
66
238
|
}
|
|
67
239
|
else {
|
|
68
240
|
eventIds.push(event.id);
|
|
69
241
|
}
|
|
70
|
-
Object.assign(context, payload ?? {});
|
|
242
|
+
Object.assign(context, typed.payload ?? {});
|
|
71
243
|
break;
|
|
72
244
|
case "child_changed":
|
|
73
245
|
case "child_delivered":
|
|
74
246
|
case "child_regressed":
|
|
75
247
|
if (!runType) {
|
|
76
248
|
runType = "implementation";
|
|
77
|
-
wakeReason =
|
|
249
|
+
wakeReason = typed.eventType;
|
|
78
250
|
eventIds = [event.id];
|
|
79
251
|
}
|
|
80
252
|
else {
|
|
81
253
|
eventIds.push(event.id);
|
|
82
254
|
}
|
|
83
|
-
Object.assign(context, payload ?? {});
|
|
255
|
+
Object.assign(context, typed.payload ?? {});
|
|
84
256
|
resumeThread = true;
|
|
85
257
|
break;
|
|
86
258
|
case "direct_reply": {
|
|
@@ -92,14 +264,12 @@ export function deriveSessionWakePlan(issue, events) {
|
|
|
92
264
|
else {
|
|
93
265
|
eventIds.push(event.id);
|
|
94
266
|
}
|
|
95
|
-
const text =
|
|
96
|
-
? payload.text
|
|
97
|
-
: typeof payload?.body === "string" ? payload.body : undefined;
|
|
267
|
+
const text = typed.payload?.text ?? typed.payload?.body;
|
|
98
268
|
if (text) {
|
|
99
269
|
followUps.push({
|
|
100
|
-
type:
|
|
270
|
+
type: typed.eventType,
|
|
101
271
|
text,
|
|
102
|
-
...(
|
|
272
|
+
...(typed.payload?.author !== undefined ? { author: typed.payload.author } : {}),
|
|
103
273
|
});
|
|
104
274
|
}
|
|
105
275
|
context.directReplyMode = true;
|
|
@@ -108,7 +278,7 @@ export function deriveSessionWakePlan(issue, events) {
|
|
|
108
278
|
}
|
|
109
279
|
case "completion_check_continue": {
|
|
110
280
|
if (!runType) {
|
|
111
|
-
runType = parseRunType(payload?.runType)
|
|
281
|
+
runType = parseRunType(typed.payload?.runType)
|
|
112
282
|
?? (issue.prReviewState === "changes_requested" ? "review_fix" : "implementation");
|
|
113
283
|
wakeReason = "completion_check_continue";
|
|
114
284
|
eventIds = [event.id];
|
|
@@ -116,9 +286,9 @@ export function deriveSessionWakePlan(issue, events) {
|
|
|
116
286
|
else {
|
|
117
287
|
eventIds.push(event.id);
|
|
118
288
|
}
|
|
119
|
-
Object.assign(context, payload ?? {});
|
|
120
|
-
if (
|
|
121
|
-
context.completionCheckSummary = payload.summary.trim();
|
|
289
|
+
Object.assign(context, typed.payload ?? {});
|
|
290
|
+
if (typed.payload?.summary?.trim()) {
|
|
291
|
+
context.completionCheckSummary = typed.payload.summary.trim();
|
|
122
292
|
}
|
|
123
293
|
context.completionCheckMode = true;
|
|
124
294
|
resumeThread = true;
|
|
@@ -129,42 +299,49 @@ export function deriveSessionWakePlan(issue, events) {
|
|
|
129
299
|
case "operator_prompt": {
|
|
130
300
|
if (!runType) {
|
|
131
301
|
runType = issue.prReviewState === "changes_requested" ? "review_fix" : "implementation";
|
|
132
|
-
wakeReason = issue.issueClass === "orchestration" ? "human_instruction" :
|
|
302
|
+
wakeReason = issue.issueClass === "orchestration" ? "human_instruction" : typed.eventType;
|
|
133
303
|
eventIds = [event.id];
|
|
134
304
|
}
|
|
135
305
|
else {
|
|
136
306
|
eventIds.push(event.id);
|
|
137
307
|
}
|
|
138
|
-
const text =
|
|
139
|
-
? payload.text
|
|
140
|
-
: typeof payload?.body === "string" ? payload.body : undefined;
|
|
308
|
+
const text = typed.payload?.text ?? typed.payload?.body;
|
|
141
309
|
if (text) {
|
|
142
310
|
followUps.push({
|
|
143
|
-
type:
|
|
311
|
+
type: typed.eventType,
|
|
144
312
|
text,
|
|
145
|
-
...(
|
|
313
|
+
...(typed.payload?.author !== undefined ? { author: typed.payload.author } : {}),
|
|
146
314
|
});
|
|
147
315
|
}
|
|
148
|
-
if (payload?.replacementPrRequired === true) {
|
|
316
|
+
if (typed.payload?.replacementPrRequired === true) {
|
|
149
317
|
context.replacementPrRequired = true;
|
|
150
|
-
if (
|
|
151
|
-
context.previousPrNumber = payload.previousPrNumber;
|
|
152
|
-
if (
|
|
153
|
-
context.previousPrUrl = payload.previousPrUrl;
|
|
154
|
-
if (
|
|
155
|
-
context.previousPrState = payload.previousPrState;
|
|
156
|
-
if (
|
|
157
|
-
context.previousPrHeadSha = payload.previousPrHeadSha;
|
|
158
|
-
}
|
|
159
|
-
if (event.eventType === "followup_prompt"
|
|
160
|
-
|| event.eventType === "followup_comment"
|
|
161
|
-
|| event.eventType === "operator_prompt") {
|
|
162
|
-
resumeThread = true;
|
|
318
|
+
if (typed.payload.previousPrNumber !== undefined)
|
|
319
|
+
context.previousPrNumber = typed.payload.previousPrNumber;
|
|
320
|
+
if (typed.payload.previousPrUrl !== undefined)
|
|
321
|
+
context.previousPrUrl = typed.payload.previousPrUrl;
|
|
322
|
+
if (typed.payload.previousPrState !== undefined)
|
|
323
|
+
context.previousPrState = typed.payload.previousPrState;
|
|
324
|
+
if (typed.payload.previousPrHeadSha !== undefined)
|
|
325
|
+
context.previousPrHeadSha = typed.payload.previousPrHeadSha;
|
|
163
326
|
}
|
|
327
|
+
resumeThread = true;
|
|
164
328
|
break;
|
|
165
329
|
}
|
|
166
|
-
|
|
330
|
+
// Terminal and non-actionable events were filtered out above; listed
|
|
331
|
+
// here so the switch stays exhaustive over the union.
|
|
332
|
+
case "delegation_observed":
|
|
333
|
+
case "prompt_delivered":
|
|
334
|
+
case "self_comment":
|
|
335
|
+
case "run_released_authority":
|
|
336
|
+
case "stop_requested":
|
|
337
|
+
case "operator_closed":
|
|
338
|
+
case "undelegated":
|
|
339
|
+
case "issue_removed":
|
|
340
|
+
case "pr_closed":
|
|
341
|
+
case "pr_merged":
|
|
167
342
|
break;
|
|
343
|
+
default:
|
|
344
|
+
assertNever(typed, "Unhandled issue session event in wake derivation");
|
|
168
345
|
}
|
|
169
346
|
}
|
|
170
347
|
if (!runType)
|
|
@@ -217,14 +394,3 @@ export function extractLatestAssistantSummary(run) {
|
|
|
217
394
|
}
|
|
218
395
|
return sanitizeOperatorFacingText(run.failureReason);
|
|
219
396
|
}
|
|
220
|
-
function parseEventJson(raw) {
|
|
221
|
-
if (!raw)
|
|
222
|
-
return undefined;
|
|
223
|
-
try {
|
|
224
|
-
const parsed = JSON.parse(raw);
|
|
225
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
|
|
226
|
-
}
|
|
227
|
-
catch {
|
|
228
|
-
return undefined;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
@@ -13,19 +13,13 @@ function hasRecoveredContext(context) {
|
|
|
13
13
|
function hasLocalHumanContext(context) {
|
|
14
14
|
if (hasRecoveredContext(context))
|
|
15
15
|
return true;
|
|
16
|
-
for (const
|
|
17
|
-
const value = context?.[key];
|
|
16
|
+
for (const value of [context?.promptContext, context?.promptBody, context?.operatorPrompt, context?.userComment]) {
|
|
18
17
|
if (typeof value === "string" && value.trim().length > 0)
|
|
19
18
|
return true;
|
|
20
19
|
}
|
|
21
20
|
if (!Array.isArray(context?.followUps))
|
|
22
21
|
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
|
-
});
|
|
22
|
+
return context.followUps.some((entry) => typeof entry.text === "string" && entry.text.trim().length > 0);
|
|
29
23
|
}
|
|
30
24
|
function activitySortKey(activity) {
|
|
31
25
|
const parsed = activity.updatedAt ? Date.parse(activity.updatedAt) : NaN;
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { buildRequestedChangesWakeIdentity } from "./reactive-wake-keys.js";
|
|
2
|
-
|
|
2
|
+
import { tryParseRunContextValue } from "./run-context.js";
|
|
3
|
+
// Boundary over the stored failure/incident columns: malformed JSON or a
|
|
4
|
+
// schema-rejected legacy shape degrades to "no context" (pre-existing
|
|
5
|
+
// behavior of the old parseObjectJson for malformed JSON; this pure module
|
|
6
|
+
// has no logger to warn through).
|
|
7
|
+
function parseRunContextColumn(value) {
|
|
3
8
|
if (!value)
|
|
4
9
|
return undefined;
|
|
10
|
+
let parsed;
|
|
5
11
|
try {
|
|
6
|
-
|
|
7
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
|
|
12
|
+
parsed = JSON.parse(value);
|
|
8
13
|
}
|
|
9
14
|
catch {
|
|
10
15
|
return undefined;
|
|
11
16
|
}
|
|
17
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? tryParseRunContextValue(parsed) : undefined;
|
|
12
18
|
}
|
|
13
19
|
export function buildOperatorRetryEvent(issue, runType, source = "operator_retry") {
|
|
14
20
|
if (runType === "queue_repair") {
|
|
15
|
-
const queueIncident =
|
|
16
|
-
const failureContext =
|
|
21
|
+
const queueIncident = parseRunContextColumn(issue.lastQueueIncidentJson);
|
|
22
|
+
const failureContext = parseRunContextColumn(issue.lastGitHubFailureContextJson);
|
|
17
23
|
return {
|
|
18
24
|
eventType: "merge_steward_incident",
|
|
19
25
|
eventJson: JSON.stringify({
|
|
@@ -32,7 +38,7 @@ export function buildOperatorRetryEvent(issue, runType, source = "operator_retry
|
|
|
32
38
|
};
|
|
33
39
|
}
|
|
34
40
|
if (runType === "ci_repair") {
|
|
35
|
-
const failureContext =
|
|
41
|
+
const failureContext = parseRunContextColumn(issue.lastGitHubFailureContextJson);
|
|
36
42
|
return {
|
|
37
43
|
eventType: "settled_red_ci",
|
|
38
44
|
eventJson: JSON.stringify({
|