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
package/dist/run-wake-planner.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { getCiRepairBudget, getQueueRepairBudget, getReviewFixBudget, } from "./run-budgets.js";
|
|
2
2
|
import { buildRequestedChangesWakeIdentity } from "./reactive-wake-keys.js";
|
|
3
|
+
import { parseRunContextOrWarn, serializeRunContext } from "./run-context.js";
|
|
4
|
+
import { assertNever } from "./utils.js";
|
|
3
5
|
const WRITER = "run-wake-planner";
|
|
4
6
|
export class RunWakePlanner {
|
|
5
7
|
db;
|
|
6
|
-
|
|
8
|
+
logger;
|
|
9
|
+
constructor(db, logger) {
|
|
7
10
|
this.db = db;
|
|
11
|
+
this.logger = logger;
|
|
8
12
|
}
|
|
9
13
|
resolveRunWake(issue) {
|
|
10
14
|
const sessionWake = this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId);
|
|
@@ -22,46 +26,52 @@ export class RunWakePlanner {
|
|
|
22
26
|
let eventType;
|
|
23
27
|
let dedupeKey;
|
|
24
28
|
let eventContext = context;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
29
|
+
switch (runType) {
|
|
30
|
+
case "queue_repair":
|
|
31
|
+
eventType = "merge_steward_incident";
|
|
32
|
+
dedupeKey = `${dedupeScope ?? "wake"}:queue_repair:${issue.linearIssueId}:${issue.prHeadSha ?? issue.lastGitHubFailureHeadSha ?? "unknown-sha"}`;
|
|
33
|
+
break;
|
|
34
|
+
case "ci_repair":
|
|
35
|
+
eventType = "settled_red_ci";
|
|
36
|
+
dedupeKey = `${dedupeScope ?? "wake"}:ci_repair:${issue.linearIssueId}:${issue.lastGitHubFailureSignature ?? issue.prHeadSha ?? "unknown-sha"}`;
|
|
37
|
+
break;
|
|
38
|
+
case "review_fix":
|
|
39
|
+
case "branch_upkeep": {
|
|
40
|
+
eventType = "review_changes_requested";
|
|
41
|
+
const identity = buildRequestedChangesWakeIdentity({
|
|
42
|
+
linearIssueId: issue.linearIssueId,
|
|
43
|
+
runType,
|
|
44
|
+
headSha: issue.prHeadSha,
|
|
45
|
+
});
|
|
46
|
+
dedupeKey = identity.dedupeKey;
|
|
47
|
+
eventContext = {
|
|
48
|
+
...context,
|
|
49
|
+
requestedChangesCoalesceKey: identity.coalesceKey,
|
|
50
|
+
...(identity.headSha ? { requestedChangesHeadSha: identity.headSha } : {}),
|
|
51
|
+
};
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case "implementation":
|
|
55
|
+
eventType = "delegated";
|
|
56
|
+
dedupeKey = `${dedupeScope ?? "wake"}:implementation:${issue.linearIssueId}`;
|
|
57
|
+
break;
|
|
58
|
+
default:
|
|
59
|
+
return assertNever(runType, "Unhandled run type in wake event append");
|
|
50
60
|
}
|
|
51
61
|
return Boolean(this.db.issueSessions.appendIssueSessionEventWithLease(lease, {
|
|
52
62
|
projectId: issue.projectId,
|
|
53
63
|
linearIssueId: issue.linearIssueId,
|
|
54
64
|
eventType,
|
|
55
|
-
...(eventContext ? { eventJson:
|
|
65
|
+
...(eventContext ? { eventJson: serializeRunContext(eventContext, "wake event context") } : {}),
|
|
56
66
|
dedupeKey,
|
|
57
67
|
}));
|
|
58
68
|
}
|
|
59
69
|
materializeLegacyPendingWake(issue, lease) {
|
|
60
70
|
if (!issue.pendingRunType)
|
|
61
71
|
return issue;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
// Boundary over possibly-old DB rows: a legacy pending context that no
|
|
73
|
+
// longer parses is dropped (with a warning) instead of wedging the wake.
|
|
74
|
+
const context = parseRunContextOrWarn(issue.pendingRunContextJson, (message) => this.logger?.warn({ issueKey: issue.issueKey, linearIssueId: issue.linearIssueId }, `Dropping unparseable legacy pending run context: ${message}`), "legacy pending run context");
|
|
65
75
|
this.appendWakeEventWithLease(lease, issue, issue.pendingRunType, context, "legacy_pending");
|
|
66
76
|
const commit = this.db.issueSessions.commitIssueState({
|
|
67
77
|
writer: WRITER,
|
package/dist/status-note.js
CHANGED
|
@@ -1,42 +1,60 @@
|
|
|
1
1
|
import { extractCompletionCheck } from "./completion-check.js";
|
|
2
|
-
import { extractLatestAssistantSummary } from "./issue-session-events.js";
|
|
2
|
+
import { extractLatestAssistantSummary, parseIssueSessionEventOrWarn } from "./issue-session-events.js";
|
|
3
3
|
import { sanitizeOperatorFacingText } from "./presentation-text.js";
|
|
4
|
+
import { assertNever } from "./utils.js";
|
|
4
5
|
function clean(value) {
|
|
5
6
|
return sanitizeOperatorFacingText(value);
|
|
6
7
|
}
|
|
8
|
+
function dirtyWorktreeSummary(payload) {
|
|
9
|
+
return payload?.dirtyWorktree === true ? payload.summary : undefined;
|
|
10
|
+
}
|
|
7
11
|
function eventStatusNote(event) {
|
|
8
12
|
if (!event)
|
|
9
13
|
return undefined;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Read-model boundary over possibly-old DB rows: degrade a bad payload to
|
|
15
|
+
// the payload-less note instead of failing the status view.
|
|
16
|
+
const typed = parseIssueSessionEventOrWarn(event);
|
|
17
|
+
if (!typed)
|
|
18
|
+
return undefined;
|
|
19
|
+
switch (typed.eventType) {
|
|
20
|
+
case "stop_requested": {
|
|
21
|
+
const dirtySummary = dirtyWorktreeSummary(typed.payload);
|
|
16
22
|
if (dirtySummary)
|
|
17
23
|
return `Operator stopped the run with dirty worktree: ${dirtySummary}. Use retry or delegate again to resume.`;
|
|
18
24
|
return "Operator stopped the run. Use retry or delegate again to resume.";
|
|
19
|
-
|
|
25
|
+
}
|
|
26
|
+
case "undelegated": {
|
|
27
|
+
const dirtySummary = dirtyWorktreeSummary(typed.payload);
|
|
20
28
|
if (dirtySummary)
|
|
21
29
|
return `Issue was un-delegated from PatchRelay with dirty worktree: ${dirtySummary}`;
|
|
22
30
|
return "Issue was un-delegated from PatchRelay. Delegate it again to resume.";
|
|
31
|
+
}
|
|
23
32
|
case "issue_removed":
|
|
24
33
|
return "Issue was removed from Linear.";
|
|
25
34
|
case "pr_closed":
|
|
26
35
|
return "Pull request was closed without merging.";
|
|
27
36
|
case "pr_merged":
|
|
28
37
|
return "Pull request merged successfully.";
|
|
29
|
-
|
|
38
|
+
case "delegated":
|
|
39
|
+
case "delegation_observed":
|
|
40
|
+
case "child_changed":
|
|
41
|
+
case "child_delivered":
|
|
42
|
+
case "child_regressed":
|
|
43
|
+
case "direct_reply":
|
|
44
|
+
case "completion_check_continue":
|
|
45
|
+
case "followup_prompt":
|
|
46
|
+
case "followup_comment":
|
|
47
|
+
case "prompt_delivered":
|
|
48
|
+
case "self_comment":
|
|
49
|
+
case "operator_prompt":
|
|
50
|
+
case "review_changes_requested":
|
|
51
|
+
case "settled_red_ci":
|
|
52
|
+
case "merge_steward_incident":
|
|
53
|
+
case "operator_closed":
|
|
54
|
+
case "run_released_authority":
|
|
30
55
|
return undefined;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
function parseEventJson(value) {
|
|
34
|
-
try {
|
|
35
|
-
const parsed = JSON.parse(value);
|
|
36
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return undefined;
|
|
56
|
+
default:
|
|
57
|
+
return assertNever(typed, "Unhandled issue session event in status note");
|
|
40
58
|
}
|
|
41
59
|
}
|
|
42
60
|
export function deriveIssueStatusNote(params) {
|
package/dist/utils.js
CHANGED
|
@@ -87,6 +87,15 @@ export function safeJsonParse(value) {
|
|
|
87
87
|
return undefined;
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Exhaustiveness guard for discriminated-union switches (plan §D2): the call
|
|
92
|
+
* only typechecks when every union member is handled, so adding a new variant
|
|
93
|
+
* fails compilation at every consumer. At runtime it throws — reaching it
|
|
94
|
+
* means a value outside the union leaked past a parse boundary.
|
|
95
|
+
*/
|
|
96
|
+
export function assertNever(value, message = "Unexpected value") {
|
|
97
|
+
throw new Error(`${message}: ${JSON.stringify(value)}`);
|
|
98
|
+
}
|
|
90
99
|
export function redactSensitiveHeaders(headers) {
|
|
91
100
|
return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, REDACTED_HEADER_NAMES.has(name.toLowerCase()) ? "[redacted]" : value]));
|
|
92
101
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { serializeRunContext } from "../run-context.js";
|
|
1
2
|
/**
|
|
2
3
|
* Picks the single factoryState the caller should write. Priority is the
|
|
3
4
|
* inverse of the previous spread order — the LAST spread wins in JS, so we
|
|
@@ -48,7 +49,7 @@ function buildStartupResumeContextJson(input) {
|
|
|
48
49
|
if (input.startupResume.pendingRunType === undefined)
|
|
49
50
|
return undefined;
|
|
50
51
|
return input.startupResume.pendingRunContext
|
|
51
|
-
?
|
|
52
|
+
? serializeRunContext(input.startupResume.pendingRunContext, "startup resume context")
|
|
52
53
|
: null;
|
|
53
54
|
}
|
|
54
55
|
export function resolveIssueUpdatePlan(input) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
2
|
+
import { tryParseRunContextValue } from "./run-context.js";
|
|
2
3
|
function parseObjectJson(raw) {
|
|
3
4
|
if (!raw)
|
|
4
5
|
return undefined;
|
|
@@ -12,6 +13,17 @@ function parseObjectJson(raw) {
|
|
|
12
13
|
return undefined;
|
|
13
14
|
}
|
|
14
15
|
}
|
|
16
|
+
// Boundary over reconciliation columns (failure context / queue incident /
|
|
17
|
+
// CI snapshot JSON) merged into an implicit wake's run context. Degrading a
|
|
18
|
+
// schema-rejected value to "no context" matches the pre-existing behavior of
|
|
19
|
+
// parseObjectJson for malformed JSON; the persistence layer has no logger to
|
|
20
|
+
// warn through.
|
|
21
|
+
function parseRunContextColumn(raw) {
|
|
22
|
+
const value = parseObjectJson(raw);
|
|
23
|
+
if (!value)
|
|
24
|
+
return undefined;
|
|
25
|
+
return tryParseRunContextValue(value);
|
|
26
|
+
}
|
|
15
27
|
function hasUnattemptedFailureSignature(issue, fallbackHeadSha) {
|
|
16
28
|
const signature = issue.lastGitHubFailureSignature;
|
|
17
29
|
if (!signature)
|
|
@@ -33,11 +45,11 @@ export function deriveImplicitReactiveWake(issue) {
|
|
|
33
45
|
if (!reactiveIntent)
|
|
34
46
|
return undefined;
|
|
35
47
|
if (reactiveIntent.runType === "ci_repair") {
|
|
36
|
-
const failureContext =
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
const failureContext = parseRunContextColumn(issue.lastGitHubFailureContextJson) ?? {};
|
|
49
|
+
const snapshotValue = parseObjectJson(issue.lastGitHubCiSnapshotJson);
|
|
50
|
+
const snapshot = snapshotValue ? tryParseRunContextValue({ ciSnapshot: snapshotValue })?.ciSnapshot : undefined;
|
|
51
|
+
const fallbackHeadSha = failureContext.failureHeadSha
|
|
52
|
+
?? issue.lastGitHubFailureHeadSha ?? issue.prHeadSha;
|
|
41
53
|
const failureSignature = issue.lastGitHubFailureSignature
|
|
42
54
|
?? (fallbackHeadSha ? `implicit_branch_ci::${fallbackHeadSha}` : undefined);
|
|
43
55
|
if (!failureSignature || issue.prState !== "open")
|
|
@@ -59,11 +71,9 @@ export function deriveImplicitReactiveWake(issue) {
|
|
|
59
71
|
};
|
|
60
72
|
}
|
|
61
73
|
if (reactiveIntent.runType === "queue_repair") {
|
|
62
|
-
const failureContext =
|
|
63
|
-
const incidentContext =
|
|
64
|
-
const fallbackHeadSha =
|
|
65
|
-
? failureContext.failureHeadSha
|
|
66
|
-
: undefined;
|
|
74
|
+
const failureContext = parseRunContextColumn(issue.lastGitHubFailureContextJson) ?? {};
|
|
75
|
+
const incidentContext = parseRunContextColumn(issue.lastQueueIncidentJson) ?? {};
|
|
76
|
+
const fallbackHeadSha = failureContext.failureHeadSha;
|
|
67
77
|
if (!hasUnattemptedFailureSignature(issue, fallbackHeadSha))
|
|
68
78
|
return undefined;
|
|
69
79
|
return {
|