patchrelay 0.78.0 → 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 +2 -2
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Plan §D1: the one typed schema for the run context object — the bag that is
|
|
3
|
+
// (a) stored in `issues.pending_run_context_json` (legacy pending-wake path,
|
|
4
|
+
// read back by RunWakePlanner.materializeLegacyPendingWake),
|
|
5
|
+
// (b) carried in session-event `event_json` payloads for wake events and
|
|
6
|
+
// merged into the wake plan by deriveSessionWakePlan, and
|
|
7
|
+
// (c) passed around in memory as `context` / `effectiveContext` /
|
|
8
|
+
// `pendingRunContext` until it reaches the prompt builder and launcher.
|
|
9
|
+
//
|
|
10
|
+
// Every known field is typed strictly so a mistyped field fails loudly at the
|
|
11
|
+
// parse boundary. Unknown keys are deliberately TOLERATED (loose object), not
|
|
12
|
+
// rejected, because:
|
|
13
|
+
// - existing DB rows contain contexts written by older PatchRelay versions
|
|
14
|
+
// whose field sets we no longer produce (e.g. `mergeQueueContext`,
|
|
15
|
+
// `userComment`, `operatorPrompt` below survive only as legacy reads), and
|
|
16
|
+
// - deriveSessionWakePlan merges whole event payloads into the context via
|
|
17
|
+
// Object.assign, so producer-side extra keys flow through by design.
|
|
18
|
+
// The static `RunContext` type intentionally has NO index signature (it is
|
|
19
|
+
// inferred from a non-loose mirror of the same shape), so compile-time access
|
|
20
|
+
// to undeclared fields is an error even though runtime parsing passes unknown
|
|
21
|
+
// keys through. NESTED objects (ciSnapshot, incidentContext, reviewComments
|
|
22
|
+
// entries, ...) use plain z.object — unknown nested keys are stripped at the
|
|
23
|
+
// boundary instead of passed through: they are leaf display data, every field
|
|
24
|
+
// any consumer reads is declared here, and a single definition keeps the
|
|
25
|
+
// static type free of index signatures so producer-side `satisfies RunContext`
|
|
26
|
+
// checks stay sound.
|
|
27
|
+
/** Entry of `followUps`, assembled by deriveSessionWakePlan from
|
|
28
|
+
* direct_reply / followup_prompt / followup_comment / operator_prompt event
|
|
29
|
+
* payloads; consumed by prompting/patchrelay.ts buildFollowUpContextLines. */
|
|
30
|
+
const followUpEntryShape = {
|
|
31
|
+
type: z.string().optional(),
|
|
32
|
+
text: z.string().optional(),
|
|
33
|
+
author: z.string().optional(),
|
|
34
|
+
};
|
|
35
|
+
/** Inline review comment captured from GitHub. Produced by
|
|
36
|
+
* github-webhook-reactive-run.ts fetchReviewCommentsForEvent and
|
|
37
|
+
* reactive-run-policy.ts hydrateRequestedChangesContext (remote-pr-review.ts);
|
|
38
|
+
* consumed by prompting/patchrelay.ts readReviewFixComments and
|
|
39
|
+
* run-orchestrator.ts (review round activity comment count). */
|
|
40
|
+
const reviewCommentShape = {
|
|
41
|
+
id: z.number().optional(),
|
|
42
|
+
body: z.string().optional(),
|
|
43
|
+
path: z.string().optional(),
|
|
44
|
+
line: z.number().optional(),
|
|
45
|
+
side: z.string().optional(),
|
|
46
|
+
startLine: z.number().optional(),
|
|
47
|
+
startSide: z.string().optional(),
|
|
48
|
+
commitId: z.string().optional(),
|
|
49
|
+
url: z.string().optional(),
|
|
50
|
+
diffHunk: z.string().optional(),
|
|
51
|
+
authorLogin: z.string().optional(),
|
|
52
|
+
};
|
|
53
|
+
/** Related-issue summary used by the issue-topology prompt sections. Produced
|
|
54
|
+
* by run-orchestrator.ts buildRelatedIssueContext; consumed by
|
|
55
|
+
* prompting/patchrelay.ts summarizeRelationEntries. */
|
|
56
|
+
const relatedIssueShape = {
|
|
57
|
+
linearIssueId: z.string().optional(),
|
|
58
|
+
issueKey: z.string().optional(),
|
|
59
|
+
title: z.string().optional(),
|
|
60
|
+
stateName: z.string().optional(),
|
|
61
|
+
stateType: z.string().optional(),
|
|
62
|
+
factoryState: z.string().optional(),
|
|
63
|
+
currentLinearState: z.string().optional(),
|
|
64
|
+
delegatedToPatchRelay: z.boolean().optional(),
|
|
65
|
+
hasOpenPr: z.boolean().optional(),
|
|
66
|
+
};
|
|
67
|
+
/** One check inside a CI snapshot (github-failure-context.ts
|
|
68
|
+
* mapCiSnapshotCheck). */
|
|
69
|
+
const ciSnapshotCheckShape = {
|
|
70
|
+
name: z.string().optional(),
|
|
71
|
+
status: z.string().optional(),
|
|
72
|
+
conclusion: z.string().optional(),
|
|
73
|
+
detailsUrl: z.string().optional(),
|
|
74
|
+
summary: z.string().optional(),
|
|
75
|
+
};
|
|
76
|
+
/** Settled CI snapshot. Produced by github-failure-context.ts
|
|
77
|
+
* buildCiSnapshotFromChecks (attached to settled_red_ci payloads by
|
|
78
|
+
* github-webhook-reactive-run.ts and to implicit ci_repair wakes by
|
|
79
|
+
* workflow-wake-resolver.ts); consumed by prompting/patchrelay.ts
|
|
80
|
+
* buildCiRepairContext. */
|
|
81
|
+
const ciSnapshotShape = {
|
|
82
|
+
headSha: z.string().optional(),
|
|
83
|
+
gateCheckName: z.string().optional(),
|
|
84
|
+
gateCheckStatus: z.string().optional(),
|
|
85
|
+
settledAt: z.string().optional(),
|
|
86
|
+
capturedAt: z.string().optional(),
|
|
87
|
+
failedChecks: z.array(z.object(ciSnapshotCheckShape)).optional(),
|
|
88
|
+
checks: z.array(z.object(ciSnapshotCheckShape)).optional(),
|
|
89
|
+
};
|
|
90
|
+
/** Queue-eviction incident detail (merge-queue-incident.ts
|
|
91
|
+
* QueueEvictionIncidentContext), parsed from the steward's check-run output. */
|
|
92
|
+
const queueIncidentContextShape = {
|
|
93
|
+
version: z.number().optional(),
|
|
94
|
+
failureClass: z.string().optional(),
|
|
95
|
+
baseSha: z.string().optional(),
|
|
96
|
+
prHeadSha: z.string().optional(),
|
|
97
|
+
queuePosition: z.number().optional(),
|
|
98
|
+
baseBranch: z.string().optional(),
|
|
99
|
+
branch: z.string().optional(),
|
|
100
|
+
issueKey: z.string().nullable().optional(),
|
|
101
|
+
conflictFiles: z.array(z.string()).optional(),
|
|
102
|
+
failedChecks: z.array(z.object({
|
|
103
|
+
name: z.string().optional(),
|
|
104
|
+
conclusion: z.string().optional(),
|
|
105
|
+
url: z.string().optional(),
|
|
106
|
+
})).optional(),
|
|
107
|
+
retryHistory: z.array(z.object({
|
|
108
|
+
at: z.string().optional(),
|
|
109
|
+
baseSha: z.string().optional(),
|
|
110
|
+
outcome: z.string().optional(),
|
|
111
|
+
})).optional(),
|
|
112
|
+
};
|
|
113
|
+
/** LEGACY: merge-queue context block read by prompting/patchrelay.ts
|
|
114
|
+
* appendQueueRepairContext. No current producer writes this field — it only
|
|
115
|
+
* appears in contexts persisted by older versions, so it stays in the schema
|
|
116
|
+
* for legacy-row compatibility. */
|
|
117
|
+
const mergeQueueContextShape = {
|
|
118
|
+
baseBranch: z.string().optional(),
|
|
119
|
+
baseSha: z.string().optional(),
|
|
120
|
+
mergeCommitSha: z.string().optional(),
|
|
121
|
+
checkRunUrl: z.string().optional(),
|
|
122
|
+
incidentSummary: z.string().optional(),
|
|
123
|
+
conflictingFiles: z.array(z.string()).optional(),
|
|
124
|
+
operatorHints: z.array(z.string()).optional(),
|
|
125
|
+
};
|
|
126
|
+
const runContextShape = {
|
|
127
|
+
// ── Wake framing ──────────────────────────────────────────────────
|
|
128
|
+
/** Why this wake exists. Produced by deriveSessionWakePlan (and by
|
|
129
|
+
* branch-upkeep context builders, operator-retry-event); consumed by
|
|
130
|
+
* prompting/patchrelay.ts (turn reason, follow-up prompt selection). Kept a
|
|
131
|
+
* free string: the value set spans wake reasons and event types and legacy
|
|
132
|
+
* rows carry values we no longer emit. */
|
|
133
|
+
wakeReason: z.string().optional(),
|
|
134
|
+
/** Requested run type inside a `delegated` / `completion_check_continue`
|
|
135
|
+
* payload. Free string because legacy payloads carry removed run types
|
|
136
|
+
* (e.g. "main_repair"); consumers narrow via parseRunType and fall back to
|
|
137
|
+
* "implementation". */
|
|
138
|
+
runType: z.string().optional(),
|
|
139
|
+
/** Producer tag ("operator_retry", "queue_health_monitor",
|
|
140
|
+
* "idle_reconciliation", ...). Produced by operator-retry-event.ts,
|
|
141
|
+
* queue-health-monitor.ts, idle-reconciliation.ts; diagnostic only. */
|
|
142
|
+
source: z.string().optional(),
|
|
143
|
+
// ── Human / orchestration context ─────────────────────────────────
|
|
144
|
+
/** Prompt guidance. Produced by buildBranchUpkeepContext /
|
|
145
|
+
* buildReviewFixBranchUpkeepContext, operator-retry-event.ts,
|
|
146
|
+
* queue-health-monitor.ts, webhooks/desired-stage-recorder.ts; consumed by
|
|
147
|
+
* prompting/patchrelay.ts buildHumanContextLines. */
|
|
148
|
+
promptContext: z.string().optional(),
|
|
149
|
+
/** Latest human instruction body, from `delegated` payloads
|
|
150
|
+
* (webhooks/desired-stage-recorder.ts); consumed by buildHumanContextLines. */
|
|
151
|
+
promptBody: z.string().optional(),
|
|
152
|
+
/** LEGACY: read by prompting/patchrelay.ts and
|
|
153
|
+
* linear-agent-activity-recovery.ts; no current producer. */
|
|
154
|
+
operatorPrompt: z.string().optional(),
|
|
155
|
+
/** LEGACY: read by prompting/patchrelay.ts and
|
|
156
|
+
* linear-agent-activity-recovery.ts; no current producer. */
|
|
157
|
+
userComment: z.string().optional(),
|
|
158
|
+
/** Recovered Linear agent-activity transcript. Produced by
|
|
159
|
+
* linear-agent-activity-recovery.ts summarizeLinearAgentActivities; consumed
|
|
160
|
+
* by prompting/patchrelay.ts buildHumanContextLines. */
|
|
161
|
+
linearAgentActivityContext: z.string().optional(),
|
|
162
|
+
/** Companion count for linearAgentActivityContext (same producer). */
|
|
163
|
+
linearAgentActivityCount: z.number().optional(),
|
|
164
|
+
/** Follow-up messages collected by deriveSessionWakePlan; consumed by
|
|
165
|
+
* prompting/patchrelay.ts and linear-agent-activity-recovery.ts. */
|
|
166
|
+
followUps: z.array(z.object(followUpEntryShape)).optional(),
|
|
167
|
+
/** Set by deriveSessionWakePlan when followUps is non-empty; consumed by
|
|
168
|
+
* prompting/patchrelay.ts shouldBuildFollowUpPrompt. */
|
|
169
|
+
followUpMode: z.boolean().optional(),
|
|
170
|
+
/** Produced by deriveSessionWakePlan; consumed by run-launcher.ts
|
|
171
|
+
* shouldCompactThread. */
|
|
172
|
+
followUpCount: z.number().optional(),
|
|
173
|
+
/** Produced by deriveSessionWakePlan for direct_reply events. */
|
|
174
|
+
directReplyMode: z.boolean().optional(),
|
|
175
|
+
// ── Completion-check continuation ─────────────────────────────────
|
|
176
|
+
/** Produced by deriveSessionWakePlan for completion_check_continue events. */
|
|
177
|
+
completionCheckMode: z.boolean().optional(),
|
|
178
|
+
/** Produced by deriveSessionWakePlan (from the event payload `summary`);
|
|
179
|
+
* consumed by prompting/patchrelay.ts buildFollowUpContextLines. */
|
|
180
|
+
completionCheckSummary: z.string().optional(),
|
|
181
|
+
// ── Dirty-worktree continuation (run-finalizer.ts
|
|
182
|
+
// continueDirtyRepairWorktree → completion_check_continue payload) ──
|
|
183
|
+
/** Consumed by run-launcher.ts shouldPreserveDirtyWorktreeBeforeLaunch and
|
|
184
|
+
* prompting/patchrelay.ts buildFollowUpContextLines. */
|
|
185
|
+
preserveDirtyWorktree: z.boolean().optional(),
|
|
186
|
+
dirtyWorktreeSummary: z.string().optional(),
|
|
187
|
+
dirtyWorktreeChangedPaths: z.array(z.string()).optional(),
|
|
188
|
+
dirtyWorktreeMergeInProgress: z.boolean().optional(),
|
|
189
|
+
// ── Replacement-PR facts (agent-input-service payloads merged by
|
|
190
|
+
// deriveSessionWakePlan; consumed by prompting/patchrelay.ts) ──
|
|
191
|
+
replacementPrRequired: z.boolean().optional(),
|
|
192
|
+
previousPrNumber: z.number().optional(),
|
|
193
|
+
previousPrUrl: z.string().optional(),
|
|
194
|
+
previousPrState: z.string().optional(),
|
|
195
|
+
previousPrHeadSha: z.string().optional(),
|
|
196
|
+
// ── Requested-changes / review fix ────────────────────────────────
|
|
197
|
+
/** Coalescing identity for review_changes_requested wakes. Produced by
|
|
198
|
+
* buildRequestedChangesWakeIdentity callers (run-wake-planner.ts,
|
|
199
|
+
* github-webhook-reactive-run.ts, operator-retry-event.ts,
|
|
200
|
+
* idle-reconciliation.ts); consumed by reactive-wake-keys.ts
|
|
201
|
+
* readRequestedChangesCoalesceKey for event coalescing. */
|
|
202
|
+
requestedChangesCoalesceKey: z.string().optional(),
|
|
203
|
+
requestedChangesHeadSha: z.string().optional(),
|
|
204
|
+
/** "branch_upkeep" is the only value ever produced (idle-reconciliation-
|
|
205
|
+
* helpers.ts buildBranchUpkeepContext, reactive-pr-state.ts
|
|
206
|
+
* buildReviewFixBranchUpkeepContext, run-failure-policy.ts); consumed by
|
|
207
|
+
* run-launcher.ts, run-failure-policy.ts resolveRetryRunType and
|
|
208
|
+
* prompting/patchrelay.ts resolveRequestedChangesMode. */
|
|
209
|
+
reviewFixMode: z.enum(["branch_upkeep"]).optional(),
|
|
210
|
+
/** Same producers/consumers as reviewFixMode (plus
|
|
211
|
+
* review_changes_requested payloads from operator-retry-event.ts and
|
|
212
|
+
* deriveSessionWakePlan branch selection). */
|
|
213
|
+
branchUpkeepRequired: z.boolean().optional(),
|
|
214
|
+
/** GitHub review id. Produced by github-webhook-reactive-run.ts and
|
|
215
|
+
* reactive-run-policy.ts hydrateRequestedChangesContext; consumed by
|
|
216
|
+
* prompting/patchrelay.ts buildStructuredReviewContext. */
|
|
217
|
+
reviewId: z.number().optional(),
|
|
218
|
+
reviewCommitId: z.string().optional(),
|
|
219
|
+
reviewUrl: z.string().optional(),
|
|
220
|
+
reviewerName: z.string().optional(),
|
|
221
|
+
reviewBody: z.string().optional(),
|
|
222
|
+
reviewComments: z.array(z.object(reviewCommentShape)).optional(),
|
|
223
|
+
/** Produced by reactive-run-policy.ts hydrateRequestedChangesContext. */
|
|
224
|
+
reviewContextStatus: z.enum(["fresh", "degraded"]).optional(),
|
|
225
|
+
reviewContextDegraded: z.boolean().optional(),
|
|
226
|
+
reviewContextDegradedReason: z.string().optional(),
|
|
227
|
+
/** Produced by reactive-run-policy.ts hydrateRequestedChangesContext. */
|
|
228
|
+
currentPrHeadSha: z.string().optional(),
|
|
229
|
+
// ── Failure provenance (CI / queue repair) ────────────────────────
|
|
230
|
+
/** Free-form failure tag: "queue_eviction" (merge-queue-incident.ts),
|
|
231
|
+
* GitHubFailureSource values (idle-reconciliation-helpers.ts
|
|
232
|
+
* buildFailureContext), "queue_eviction_missed" / "preemptive_conflict"
|
|
233
|
+
* (queue-health-monitor.ts), "merge_conflict_detected"
|
|
234
|
+
* (idle-reconciliation.ts); consumed by prompting/patchrelay.ts. */
|
|
235
|
+
failureReason: z.string().optional(),
|
|
236
|
+
failureSignature: z.string().optional(),
|
|
237
|
+
failureHeadSha: z.string().optional(),
|
|
238
|
+
/** Legacy alias for failureHeadSha still consulted by run-launcher.ts,
|
|
239
|
+
* run-orchestrator.ts and idle-reconciliation-helpers.ts
|
|
240
|
+
* isDuplicateRepairAttempt; also set by reactive-run-policy.ts
|
|
241
|
+
* hydrateRequestedChangesContext (current PR head). */
|
|
242
|
+
headSha: z.string().optional(),
|
|
243
|
+
/** Produced by buildBranchUpkeepContext / buildReviewFixBranchUpkeepContext
|
|
244
|
+
* (head that was failing/dirty at wake time). */
|
|
245
|
+
failingHeadSha: z.string().optional(),
|
|
246
|
+
// GitHubFailureContext fields (github-failure-context.ts), spread into
|
|
247
|
+
// contexts by buildFailureContext / workflow-wake-resolver.ts /
|
|
248
|
+
// operator-retry-event.ts; consumed by prompting/patchrelay.ts.
|
|
249
|
+
checkName: z.string().optional(),
|
|
250
|
+
checkUrl: z.string().optional(),
|
|
251
|
+
checkDetailsUrl: z.string().optional(),
|
|
252
|
+
jobName: z.string().optional(),
|
|
253
|
+
stepName: z.string().optional(),
|
|
254
|
+
summary: z.string().optional(),
|
|
255
|
+
annotations: z.array(z.string()).optional(),
|
|
256
|
+
workflowRunId: z.number().optional(),
|
|
257
|
+
workflowName: z.string().optional(),
|
|
258
|
+
repoFullName: z.string().optional(),
|
|
259
|
+
capturedAt: z.string().optional(),
|
|
260
|
+
/** Check classification from github-webhook-failure-context.ts
|
|
261
|
+
* resolveGitHubCheckClass, attached to settled_red_ci payloads. */
|
|
262
|
+
checkClass: z.string().optional(),
|
|
263
|
+
/** See ciSnapshotShape. */
|
|
264
|
+
ciSnapshot: z.object(ciSnapshotShape).optional(),
|
|
265
|
+
// ── Queue repair (merge-queue-incident.ts QueueRepairContext) ─────
|
|
266
|
+
incidentId: z.string().optional(),
|
|
267
|
+
incidentUrl: z.string().optional(),
|
|
268
|
+
incidentTitle: z.string().optional(),
|
|
269
|
+
incidentSummary: z.string().optional(),
|
|
270
|
+
incidentContext: z.object(queueIncidentContextShape).optional(),
|
|
271
|
+
/** LEGACY (see mergeQueueContextShape). */
|
|
272
|
+
mergeQueueContext: z.object(mergeQueueContextShape).optional(),
|
|
273
|
+
queuePosition: z.number().optional(),
|
|
274
|
+
/** Force a fresh PR head SHA on queue repair. Produced by
|
|
275
|
+
* queue-health-monitor.ts and operator-retry-event.ts; consumed by
|
|
276
|
+
* prompting/patchrelay.ts buildPublicationContract and
|
|
277
|
+
* queue-health-monitor.ts isDuplicateProbe. */
|
|
278
|
+
requiresFreshHead: z.boolean().optional(),
|
|
279
|
+
// ── Branch upkeep facts ───────────────────────────────────────────
|
|
280
|
+
/** Produced by buildBranchUpkeepContext / buildReviewFixBranchUpkeepContext;
|
|
281
|
+
* consumed by prompting/patchrelay.ts buildFollowUpContextLines. */
|
|
282
|
+
mergeStateStatus: z.string().optional(),
|
|
283
|
+
baseBranch: z.string().optional(),
|
|
284
|
+
/** Set when GitHub facts were refreshed immediately before launch;
|
|
285
|
+
* consumed by prompting/patchrelay.ts (fact-freshness line). */
|
|
286
|
+
githubFactsFresh: z.boolean().optional(),
|
|
287
|
+
// ── Issue topology (implementation coordination context) ──────────
|
|
288
|
+
/** Produced by run-orchestrator.ts buildRelatedIssueContext; consumed by
|
|
289
|
+
* prompting/patchrelay.ts buildIssueTopology / orchestration constraints. */
|
|
290
|
+
unresolvedBlockers: z.array(z.object(relatedIssueShape)).optional(),
|
|
291
|
+
childIssues: z.array(z.object(relatedIssueShape)).optional(),
|
|
292
|
+
/** LEGACY alias of childIssues, still read by prompting/patchrelay.ts. */
|
|
293
|
+
trackedDependents: z.array(z.object(relatedIssueShape)).optional(),
|
|
294
|
+
// ── Child-event facts (orchestration-parent-wake.ts payloads merged by
|
|
295
|
+
// deriveSessionWakePlan for child_changed / child_delivered /
|
|
296
|
+
// child_regressed) ──
|
|
297
|
+
childIssueId: z.string().optional(),
|
|
298
|
+
childIssueKey: z.string().optional(),
|
|
299
|
+
childTitle: z.string().optional(),
|
|
300
|
+
factoryState: z.string().optional(),
|
|
301
|
+
currentLinearState: z.string().optional(),
|
|
302
|
+
prNumber: z.number().optional(),
|
|
303
|
+
prState: z.string().optional(),
|
|
304
|
+
changeKind: z.string().optional(),
|
|
305
|
+
};
|
|
306
|
+
// Type source: no index signature, so reading an undeclared field is a
|
|
307
|
+
// compile-time error at every consumer.
|
|
308
|
+
const runContextTypeSchema = z.object(runContextShape);
|
|
309
|
+
// Parse source: tolerates unknown keys (legacy rows / merged event payloads —
|
|
310
|
+
// see module comment) while still failing loudly on mistyped known fields.
|
|
311
|
+
export const runContextSchema = z.looseObject(runContextShape);
|
|
312
|
+
export class RunContextParseError extends Error {
|
|
313
|
+
constructor(message, options) {
|
|
314
|
+
super(message, options);
|
|
315
|
+
this.name = "RunContextParseError";
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Validate an already-parsed value as a run context. FAILS LOUDLY
|
|
320
|
+
* (RunContextParseError) on non-object values or mistyped known fields —
|
|
321
|
+
* that is the point of D1; callers at legacy-row boundaries may catch,
|
|
322
|
+
* warn, and treat the context as absent.
|
|
323
|
+
*/
|
|
324
|
+
export function parseRunContextValue(value, where = "run context") {
|
|
325
|
+
const result = runContextSchema.safeParse(value);
|
|
326
|
+
if (!result.success) {
|
|
327
|
+
throw new RunContextParseError(`Invalid ${where}: ${result.error.issues
|
|
328
|
+
.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
|
|
329
|
+
.join("; ")}`, { cause: result.error });
|
|
330
|
+
}
|
|
331
|
+
return result.data;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Parse a stored run-context JSON string. Returns undefined for
|
|
335
|
+
* null/undefined/empty input. FAILS LOUDLY (RunContextParseError) on
|
|
336
|
+
* malformed JSON or schema violations — no silent fallback.
|
|
337
|
+
*/
|
|
338
|
+
export function parseRunContext(json, where = "run context") {
|
|
339
|
+
if (json === null || json === undefined || json.trim() === "")
|
|
340
|
+
return undefined;
|
|
341
|
+
let parsed;
|
|
342
|
+
try {
|
|
343
|
+
parsed = JSON.parse(json);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
throw new RunContextParseError(`Malformed ${where} JSON: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
347
|
+
}
|
|
348
|
+
return parseRunContextValue(parsed, where);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Boundary helper for sites that ingest possibly-old DB rows: parse loudly,
|
|
352
|
+
* but on failure report through `warn` and degrade to "no context" instead of
|
|
353
|
+
* unwinding the caller. The parse itself never silently coerces.
|
|
354
|
+
*/
|
|
355
|
+
export function parseRunContextOrWarn(json, warn, where = "run context") {
|
|
356
|
+
try {
|
|
357
|
+
return parseRunContext(json, where);
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
warn(error instanceof Error ? error.message : String(error));
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Non-throwing variant for boundaries inside the persistence layer where no
|
|
366
|
+
* logger is plumbed (workflow-wake-resolver assembling implicit wake contexts
|
|
367
|
+
* from reconciliation columns): a value the schema rejects degrades to
|
|
368
|
+
* "no context", which was already the legacy behavior for malformed JSON in
|
|
369
|
+
* those columns. Everywhere a logger exists, prefer parseRunContextOrWarn so
|
|
370
|
+
* the failure is at least observable.
|
|
371
|
+
*/
|
|
372
|
+
export function tryParseRunContextValue(value) {
|
|
373
|
+
const result = runContextSchema.safeParse(value);
|
|
374
|
+
return result.success ? result.data : undefined;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Serialize a run context for storage. Round-trips through the schema so
|
|
378
|
+
* writers cannot persist a shape the parser would reject.
|
|
379
|
+
*/
|
|
380
|
+
export function serializeRunContext(context, where = "run context") {
|
|
381
|
+
return JSON.stringify(parseRunContextValue(context, where));
|
|
382
|
+
}
|
|
@@ -2,6 +2,7 @@ import { buildRunFailureActivity } from "./linear-session-reporting.js";
|
|
|
2
2
|
import { getRemainingZombieRecoveryDelayMs, getZombieRecoveryBudget } from "./run-budgets.js";
|
|
3
3
|
import { resolvePostRunFactoryState } from "./run-completion-policy.js";
|
|
4
4
|
import { isRequestedChangesRunType } from "./reactive-pr-state.js";
|
|
5
|
+
import { serializeRunContext } from "./run-context.js";
|
|
5
6
|
import { settleRun } from "./run-settlement.js";
|
|
6
7
|
const WRITER = "run-failure-policy";
|
|
7
8
|
// Roll back the attempt counter consumed by the interrupted run and clear the
|
|
@@ -431,7 +432,7 @@ export class RunFailurePolicy {
|
|
|
431
432
|
projectId: run.projectId,
|
|
432
433
|
linearIssueId: run.linearIssueId,
|
|
433
434
|
pendingRunType: retryRunType,
|
|
434
|
-
pendingRunContextJson: retryContext ?
|
|
435
|
+
pendingRunContextJson: retryContext ? serializeRunContext(retryContext, "requested-changes retry context") : null,
|
|
435
436
|
},
|
|
436
437
|
});
|
|
437
438
|
this.feed?.publish({
|
package/dist/run-finalizer.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseIssueSessionEventOrWarn } from "./issue-session-events.js";
|
|
2
|
+
import { assertNever } from "./utils.js";
|
|
1
3
|
import { CLEARED_FAILURE_PROVENANCE } from "./failure-provenance.js";
|
|
2
4
|
import { buildStageReport, countEventMethods } from "./run-reporting.js";
|
|
3
5
|
import { buildRunCompletedActivity, buildRunFailureActivity } from "./linear-session-reporting.js";
|
|
@@ -8,17 +10,6 @@ import { inspectGitWorktreeStatus, isRepairRunType } from "./git-worktree-status
|
|
|
8
10
|
import { buildRunOutcomeSummary } from "./run-outcome-summary.js";
|
|
9
11
|
import { settleRun } from "./run-settlement.js";
|
|
10
12
|
const WRITER = "run-finalizer";
|
|
11
|
-
function parseEventJson(eventJson) {
|
|
12
|
-
if (!eventJson)
|
|
13
|
-
return undefined;
|
|
14
|
-
try {
|
|
15
|
-
const parsed = JSON.parse(eventJson);
|
|
16
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return undefined;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
13
|
function buildRunSummaryJson(report, outcomeSummary) {
|
|
23
14
|
return JSON.stringify({
|
|
24
15
|
latestAssistantMessage: report.assistantMessages.at(-1) ?? null,
|
|
@@ -31,15 +22,16 @@ function summarizePromptDeliveryEvents(events, run) {
|
|
|
31
22
|
let delivered = 0;
|
|
32
23
|
let failed = 0;
|
|
33
24
|
for (const event of events) {
|
|
34
|
-
|
|
25
|
+
// Boundary over DB rows: a malformed payload degrades to "not counted".
|
|
26
|
+
const typed = parseIssueSessionEventOrWarn(event);
|
|
27
|
+
if (typed?.eventType !== "prompt_delivered")
|
|
35
28
|
continue;
|
|
36
|
-
|
|
37
|
-
if (payload?.runId !== run.id)
|
|
29
|
+
if (typed.payload?.runId !== run.id)
|
|
38
30
|
continue;
|
|
39
|
-
if (payload.status === "delivered") {
|
|
31
|
+
if (typed.payload.status === "delivered") {
|
|
40
32
|
delivered += 1;
|
|
41
33
|
}
|
|
42
|
-
else if (payload.status === "delivery_failed") {
|
|
34
|
+
else if (typed.payload.status === "delivery_failed") {
|
|
43
35
|
failed += 1;
|
|
44
36
|
}
|
|
45
37
|
}
|
|
@@ -94,32 +86,56 @@ export class RunFinalizer {
|
|
|
94
86
|
...(params.latestAssistantSummary ? { latestAssistantSummary: params.latestAssistantSummary } : {}),
|
|
95
87
|
};
|
|
96
88
|
const wakeEvent = this.resolveConsumedWakeEvent(params.run);
|
|
97
|
-
|
|
98
|
-
if (!wakeEvent || !payload) {
|
|
89
|
+
if (!wakeEvent) {
|
|
99
90
|
return facts;
|
|
100
91
|
}
|
|
101
|
-
|
|
92
|
+
// Boundary over DB rows: a malformed wake payload degrades to bare facts.
|
|
93
|
+
const typed = parseIssueSessionEventOrWarn(wakeEvent, (message) => this.logger.warn({ runId: params.run.id, eventId: wakeEvent.id }, message));
|
|
94
|
+
if (!typed?.payload) {
|
|
95
|
+
return facts;
|
|
96
|
+
}
|
|
97
|
+
switch (typed.eventType) {
|
|
102
98
|
case "review_changes_requested":
|
|
103
99
|
return {
|
|
104
100
|
...facts,
|
|
105
|
-
...(
|
|
106
|
-
...(
|
|
101
|
+
...(typed.payload.reviewerName !== undefined ? { reviewerName: typed.payload.reviewerName } : {}),
|
|
102
|
+
...(typed.payload.reviewBody !== undefined ? { reviewSummary: typed.payload.reviewBody } : {}),
|
|
107
103
|
};
|
|
108
104
|
case "settled_red_ci":
|
|
109
105
|
return {
|
|
110
106
|
...facts,
|
|
111
|
-
...(
|
|
112
|
-
? { failingCheckName: payload.jobName }
|
|
113
|
-
:
|
|
114
|
-
...(
|
|
107
|
+
...(typed.payload.jobName !== undefined
|
|
108
|
+
? { failingCheckName: typed.payload.jobName }
|
|
109
|
+
: typed.payload.checkName !== undefined ? { failingCheckName: typed.payload.checkName } : {}),
|
|
110
|
+
...(typed.payload.summary !== undefined ? { failureSummary: typed.payload.summary } : {}),
|
|
115
111
|
};
|
|
116
112
|
case "merge_steward_incident":
|
|
117
113
|
return {
|
|
118
114
|
...facts,
|
|
119
|
-
...(
|
|
115
|
+
...(typed.payload.incidentSummary !== undefined ? { queueIncidentSummary: typed.payload.incidentSummary } : {}),
|
|
120
116
|
};
|
|
121
|
-
|
|
117
|
+
case "delegated":
|
|
118
|
+
case "delegation_observed":
|
|
119
|
+
case "child_changed":
|
|
120
|
+
case "child_delivered":
|
|
121
|
+
case "child_regressed":
|
|
122
|
+
case "direct_reply":
|
|
123
|
+
case "completion_check_continue":
|
|
124
|
+
case "followup_prompt":
|
|
125
|
+
case "followup_comment":
|
|
126
|
+
case "prompt_delivered":
|
|
127
|
+
case "self_comment":
|
|
128
|
+
case "operator_prompt":
|
|
129
|
+
case "stop_requested":
|
|
130
|
+
case "operator_closed":
|
|
131
|
+
case "undelegated":
|
|
132
|
+
case "issue_removed":
|
|
133
|
+
case "pr_closed":
|
|
134
|
+
case "pr_merged":
|
|
135
|
+
case "run_released_authority":
|
|
122
136
|
return facts;
|
|
137
|
+
default:
|
|
138
|
+
return assertNever(typed, "Unhandled issue session event in run outcome facts");
|
|
123
139
|
}
|
|
124
140
|
}
|
|
125
141
|
buildOutcomeSummary(params) {
|
|
@@ -318,7 +334,7 @@ export class RunFinalizer {
|
|
|
318
334
|
runType: params.run.runType,
|
|
319
335
|
summary: message,
|
|
320
336
|
preserveDirtyWorktree: true,
|
|
321
|
-
dirtyWorktreeSummary: params.status.summary,
|
|
337
|
+
...(params.status.summary !== undefined ? { dirtyWorktreeSummary: params.status.summary } : {}),
|
|
322
338
|
dirtyWorktreeChangedPaths: params.status.changedPaths,
|
|
323
339
|
dirtyWorktreeMergeInProgress: params.status.mergeInProgress,
|
|
324
340
|
}),
|
package/dist/run-launcher.js
CHANGED
|
@@ -12,7 +12,7 @@ function sanitizePathSegment(value) {
|
|
|
12
12
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
13
13
|
}
|
|
14
14
|
function shouldCompactThread(issue, threadGeneration, context) {
|
|
15
|
-
const followUpCount =
|
|
15
|
+
const followUpCount = context?.followUpCount ?? 0;
|
|
16
16
|
return issue.threadId !== undefined
|
|
17
17
|
&& (threadGeneration ?? 0) >= 4
|
|
18
18
|
&& followUpCount >= 4;
|
|
@@ -125,10 +125,8 @@ export class RunLauncher {
|
|
|
125
125
|
...(params.sourceHeadSha ? { sourceHeadSha: params.sourceHeadSha } : {}),
|
|
126
126
|
promptText: params.prompt,
|
|
127
127
|
});
|
|
128
|
-
const failureHeadSha =
|
|
129
|
-
|
|
130
|
-
: typeof params.effectiveContext?.headSha === "string" ? params.effectiveContext.headSha : undefined;
|
|
131
|
-
const failureSignature = typeof params.effectiveContext?.failureSignature === "string" ? params.effectiveContext.failureSignature : undefined;
|
|
128
|
+
const failureHeadSha = params.effectiveContext?.failureHeadSha ?? params.effectiveContext?.headSha;
|
|
129
|
+
const failureSignature = params.effectiveContext?.failureSignature;
|
|
132
130
|
const claimUpdate = {
|
|
133
131
|
projectId: params.item.projectId,
|
|
134
132
|
linearIssueId: params.item.issueId,
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -134,7 +134,7 @@ export class RunOrchestrator {
|
|
|
134
134
|
this.runNotificationHandler = new RunNotificationHandler(config, db, logger, this.linearSync, this.runFinalizer, this.threadPorts.readThreadWithRetry, this.leasePorts.withHeldLease, this.leasePorts.heartbeatLease, this.leasePorts.releaseLease, feed, { interruptTurn: (options) => codex.interruptTurn(options) });
|
|
135
135
|
this.runFailurePolicy = new RunFailurePolicy(db, logger, this.linearSync, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, (lease, issue, runType, context, dedupeScope) => this.appendWakeEventWithLease(lease, issue, runType, context, dedupeScope), this.wakeDispatcher, this.recoveryPorts.restoreIdleWorktree, this.runCompletionPolicy, (projectId) => this.config.projects.find((project) => project.id === projectId), feed);
|
|
136
136
|
this.runReconciler = new RunReconciler(db, logger, linearProvider, this.linearSync, this.runFailurePolicy, this.runFinalizer, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, this.threadPorts.readThreadWithRetry, (projectId) => this.config.projects.find((project) => project.id === projectId)?.github?.repoFullName, feed, telemetry);
|
|
137
|
-
this.runWakePlanner = new RunWakePlanner(db);
|
|
137
|
+
this.runWakePlanner = new RunWakePlanner(db, logger);
|
|
138
138
|
this.linearIssueProjection = new LinearIssueProjectionService(db, linearProvider, logger);
|
|
139
139
|
this.runAdmission = new RunAdmissionController(db, this.linearIssueProjection);
|
|
140
140
|
this.idleReconciler = new IdleIssueReconciler(db, config, this.wakeDispatcher, logger, feed, undefined, (issue) => this.linearSync.syncSession(issue), linearProvider);
|
|
@@ -419,11 +419,9 @@ export class RunOrchestrator {
|
|
|
419
419
|
const effectiveContext = coordinationContext
|
|
420
420
|
? { ...coordinationContext, ...baseContextWithRecoveredActivity }
|
|
421
421
|
: baseContextWithRecoveredActivity;
|
|
422
|
-
const sourceHeadSha =
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
? effectiveContext.headSha
|
|
426
|
-
: issue.prHeadSha;
|
|
422
|
+
const sourceHeadSha = effectiveContext?.failureHeadSha
|
|
423
|
+
?? effectiveContext?.headSha
|
|
424
|
+
?? issue.prHeadSha;
|
|
427
425
|
const budgetExceeded = this.runWakePlanner.budgetExceeded(issue, project, runType, isRequestedChangesRunType);
|
|
428
426
|
if (budgetExceeded) {
|
|
429
427
|
this.emitRunSkipped(item, "budget_exceeded", issue, { runType });
|
|
@@ -508,11 +506,11 @@ export class RunOrchestrator {
|
|
|
508
506
|
this.logger.info({ issueKey: issue.issueKey, runType, threadId, turnId }, `Started ${runType} run`);
|
|
509
507
|
// Emit Linear activity + plan
|
|
510
508
|
const freshIssue = this.db.issues.getIssue(item.projectId, item.issueId) ?? issue;
|
|
511
|
-
const reviewComments =
|
|
509
|
+
const reviewComments = effectiveContext?.reviewComments;
|
|
512
510
|
const reviewRoundActivity = runType === "review_fix"
|
|
513
511
|
? buildReviewRoundStartedActivity({
|
|
514
512
|
round: Math.max(1, freshIssue.reviewFixAttempts),
|
|
515
|
-
...(
|
|
513
|
+
...(effectiveContext?.reviewerName !== undefined ? { reviewerName: effectiveContext.reviewerName } : {}),
|
|
516
514
|
...(reviewComments ? { commentCount: reviewComments.length } : {}),
|
|
517
515
|
...(typeof sourceHeadSha === "string" ? { headSha: sourceHeadSha } : {}),
|
|
518
516
|
})
|