patchrelay 0.48.0 → 0.49.1
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/README.md +2 -2
- package/dist/agent-session-plan.js +12 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/data.js +40 -15
- package/dist/db/issue-store.js +87 -4
- package/dist/db/migrations.js +34 -0
- package/dist/db.js +12 -0
- package/dist/effective-active-run.js +15 -0
- package/dist/idle-reconciliation.js +19 -0
- package/dist/issue-class.js +6 -2
- package/dist/issue-overview-query.js +2 -0
- package/dist/issue-session.js +6 -0
- package/dist/linear-client.js +13 -0
- package/dist/linear-status-comment-sync.js +1 -0
- package/dist/no-pr-completion-check.js +11 -3
- package/dist/orchestration-parent-wake.js +68 -9
- package/dist/prompting/patchrelay.js +18 -12
- package/dist/run-orchestrator.js +6 -8
- package/dist/run-reconciler.js +14 -0
- package/dist/tracked-issue-list-query.js +40 -12
- package/dist/tracked-issue-projector.js +18 -8
- package/dist/tracked-issue-query.js +5 -1
- package/dist/waiting-reason.js +7 -0
- package/dist/webhooks/comment-wake-handler.js +1 -1
- package/dist/webhooks/decision-helpers.js +3 -0
- package/dist/webhooks/desired-stage-recorder.js +86 -1
- package/dist/webhooks.js +7 -0
- package/package.json +1 -1
|
@@ -1,16 +1,74 @@
|
|
|
1
1
|
import { classifyIssue } from "./issue-class.js";
|
|
2
|
+
export const ORCHESTRATION_SETTLE_WINDOW_MS = 10_000;
|
|
3
|
+
export function computeOrchestrationSettleUntil(now = Date.now()) {
|
|
4
|
+
return new Date(now + ORCHESTRATION_SETTLE_WINDOW_MS).toISOString();
|
|
5
|
+
}
|
|
6
|
+
function resolveOrchestrationIssueClass(db, issue) {
|
|
7
|
+
return classifyIssue({
|
|
8
|
+
issue,
|
|
9
|
+
childIssueCount: db.issues.listChildIssues(issue.projectId, issue.linearIssueId).length,
|
|
10
|
+
}).issueClass;
|
|
11
|
+
}
|
|
12
|
+
function unique(values) {
|
|
13
|
+
return [...new Set(values)];
|
|
14
|
+
}
|
|
15
|
+
function resolveParentIssueIds(db, child) {
|
|
16
|
+
const parentIds = [];
|
|
17
|
+
if (child.parentLinearIssueId) {
|
|
18
|
+
parentIds.push(child.parentLinearIssueId);
|
|
19
|
+
}
|
|
20
|
+
for (const blocker of db.issues.listIssueDependencies(child.projectId, child.linearIssueId)) {
|
|
21
|
+
parentIds.push(blocker.blockerLinearIssueId);
|
|
22
|
+
}
|
|
23
|
+
return unique(parentIds);
|
|
24
|
+
}
|
|
25
|
+
export function startOrchestrationSettleWindow(db, issue, now = Date.now()) {
|
|
26
|
+
const settleUntil = computeOrchestrationSettleUntil(now);
|
|
27
|
+
db.issues.upsertIssue({
|
|
28
|
+
projectId: issue.projectId,
|
|
29
|
+
linearIssueId: issue.linearIssueId,
|
|
30
|
+
orchestrationSettleUntil: settleUntil,
|
|
31
|
+
});
|
|
32
|
+
return settleUntil;
|
|
33
|
+
}
|
|
34
|
+
export function queueSettledOrchestrationIssue(params) {
|
|
35
|
+
params.db.issues.upsertIssue({
|
|
36
|
+
projectId: params.issue.projectId,
|
|
37
|
+
linearIssueId: params.issue.linearIssueId,
|
|
38
|
+
orchestrationSettleUntil: null,
|
|
39
|
+
});
|
|
40
|
+
params.db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.issue.projectId, params.issue.linearIssueId, {
|
|
41
|
+
projectId: params.issue.projectId,
|
|
42
|
+
linearIssueId: params.issue.linearIssueId,
|
|
43
|
+
eventType: "delegated",
|
|
44
|
+
eventJson: JSON.stringify({
|
|
45
|
+
...(params.promptContext
|
|
46
|
+
? { promptContext: params.promptContext }
|
|
47
|
+
: { promptContext: "The orchestration child set has settled enough to begin planning." }),
|
|
48
|
+
}),
|
|
49
|
+
dedupeKey: `delegated:orchestration_settle:${params.issue.linearIssueId}`,
|
|
50
|
+
});
|
|
51
|
+
if (params.db.issueSessions.peekIssueSessionWake(params.issue.projectId, params.issue.linearIssueId)) {
|
|
52
|
+
params.enqueueIssue?.(params.issue.projectId, params.issue.linearIssueId);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
2
57
|
export function wakeOrchestrationParentsForChildEvent(params) {
|
|
3
58
|
const parentIds = [];
|
|
4
|
-
for (const
|
|
5
|
-
const parent = params.db.issues.getIssue(params.child.projectId,
|
|
59
|
+
for (const parentIssueId of resolveParentIssueIds(params.db, params.child)) {
|
|
60
|
+
const parent = params.db.issues.getIssue(params.child.projectId, parentIssueId);
|
|
6
61
|
if (!parent || !parent.delegatedToPatchRelay) {
|
|
7
62
|
continue;
|
|
8
63
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
64
|
+
if (resolveOrchestrationIssueClass(params.db, parent) !== "orchestration") {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Before the umbrella has started its first turn, keep absorbing nearby
|
|
68
|
+
// child-set changes into the settle window instead of launching too early.
|
|
69
|
+
if (!parent.threadId && parent.activeRunId === undefined && parent.orchestrationSettleUntil) {
|
|
70
|
+
startOrchestrationSettleWindow(params.db, parent, params.now);
|
|
71
|
+
parentIds.push(parent.linearIssueId);
|
|
14
72
|
continue;
|
|
15
73
|
}
|
|
16
74
|
params.db.issueSessions.appendIssueSessionEventRespectingActiveLease(parent.projectId, parent.linearIssueId, {
|
|
@@ -25,13 +83,14 @@ export function wakeOrchestrationParentsForChildEvent(params) {
|
|
|
25
83
|
...(params.child.currentLinearState ? { currentLinearState: params.child.currentLinearState } : {}),
|
|
26
84
|
...(params.child.prNumber !== undefined ? { prNumber: params.child.prNumber } : {}),
|
|
27
85
|
...(params.child.prState ? { prState: params.child.prState } : {}),
|
|
86
|
+
...(params.changeKind ? { changeKind: params.changeKind } : {}),
|
|
28
87
|
}),
|
|
29
|
-
dedupeKey: `${params.eventType}:${parent.linearIssueId}:${params.child.linearIssueId}:${params.child.factoryState}:${params.child.prState ?? "no-pr"}`,
|
|
88
|
+
dedupeKey: `${params.eventType}:${parent.linearIssueId}:${params.child.linearIssueId}:${params.child.factoryState}:${params.changeKind ?? params.child.prState ?? "no-pr"}`,
|
|
30
89
|
});
|
|
31
90
|
if (params.db.issueSessions.peekIssueSessionWake(parent.projectId, parent.linearIssueId)) {
|
|
32
91
|
params.enqueueIssue?.(parent.projectId, parent.linearIssueId);
|
|
33
92
|
}
|
|
34
93
|
parentIds.push(parent.linearIssueId);
|
|
35
94
|
}
|
|
36
|
-
return parentIds;
|
|
95
|
+
return unique(parentIds);
|
|
37
96
|
}
|
|
@@ -106,9 +106,11 @@ function buildCoordinationGuidance(context) {
|
|
|
106
106
|
const unresolvedBlockers = Array.isArray(context?.unresolvedBlockers)
|
|
107
107
|
? context.unresolvedBlockers.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
108
108
|
: [];
|
|
109
|
-
const
|
|
110
|
-
? context.
|
|
111
|
-
:
|
|
109
|
+
const childIssues = Array.isArray(context?.childIssues)
|
|
110
|
+
? context.childIssues.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
111
|
+
: Array.isArray(context?.trackedDependents)
|
|
112
|
+
? context.trackedDependents.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
113
|
+
: [];
|
|
112
114
|
const lines = [
|
|
113
115
|
"### Coordination / Issue Topology",
|
|
114
116
|
"",
|
|
@@ -117,7 +119,7 @@ function buildCoordinationGuidance(context) {
|
|
|
117
119
|
"When child issues already own the concrete code slices, use this issue to coordinate, create or refine follow-up issues, or verify convergence. Only ship code here if this issue still has unique implementation scope that is not already owned elsewhere.",
|
|
118
120
|
"Prefer one PR per concrete implementation issue over a broad parent branch that restates overlapping child work.",
|
|
119
121
|
];
|
|
120
|
-
if (unresolvedBlockers.length === 0 &&
|
|
122
|
+
if (unresolvedBlockers.length === 0 && childIssues.length === 0) {
|
|
121
123
|
return lines;
|
|
122
124
|
}
|
|
123
125
|
lines.push("", "Known relations from PatchRelay:");
|
|
@@ -125,12 +127,12 @@ function buildCoordinationGuidance(context) {
|
|
|
125
127
|
lines.push("Unresolved blockers:");
|
|
126
128
|
lines.push(...summarizeRelationEntries(unresolvedBlockers));
|
|
127
129
|
}
|
|
128
|
-
if (
|
|
130
|
+
if (childIssues.length > 0) {
|
|
129
131
|
if (unresolvedBlockers.length > 0) {
|
|
130
132
|
lines.push("");
|
|
131
133
|
}
|
|
132
|
-
lines.push("
|
|
133
|
-
lines.push(...summarizeRelationEntries(
|
|
134
|
+
lines.push("Canonical child issues:");
|
|
135
|
+
lines.push(...summarizeRelationEntries(childIssues));
|
|
134
136
|
}
|
|
135
137
|
return lines;
|
|
136
138
|
}
|
|
@@ -166,15 +168,19 @@ function buildOrchestrationScopeDiscipline(context) {
|
|
|
166
168
|
const unresolvedBlockers = Array.isArray(context?.unresolvedBlockers)
|
|
167
169
|
? context.unresolvedBlockers.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
168
170
|
: [];
|
|
169
|
-
const
|
|
170
|
-
? context.
|
|
171
|
-
:
|
|
171
|
+
const childIssues = Array.isArray(context?.childIssues)
|
|
172
|
+
? context.childIssues.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
173
|
+
: Array.isArray(context?.trackedDependents)
|
|
174
|
+
? context.trackedDependents.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
175
|
+
: [];
|
|
172
176
|
return [
|
|
173
177
|
"## Scope Discipline",
|
|
174
178
|
"",
|
|
175
179
|
"This issue is orchestration work.",
|
|
176
180
|
"Treat it as the owner of convergence across related issues rather than as a normal code-owning implementation branch.",
|
|
177
181
|
"Inspect why this wake happened before acting.",
|
|
182
|
+
"Adopt already-existing canonical child issues when they cover the intended split.",
|
|
183
|
+
"Do not recreate child issues that already exist under this parent unless a genuinely missing required slice remains.",
|
|
178
184
|
"Do not create an overlapping umbrella PR unless this parent clearly owns unique direct cleanup work that child issues do not already cover.",
|
|
179
185
|
"If child work is still in motion, babysit the plan, record useful observations, and return to waiting.",
|
|
180
186
|
"If child work looks delivered, audit whether the original parent goal is actually satisfied.",
|
|
@@ -183,8 +189,8 @@ function buildOrchestrationScopeDiscipline(context) {
|
|
|
183
189
|
"",
|
|
184
190
|
"### Child Issue Summaries",
|
|
185
191
|
"",
|
|
186
|
-
...(
|
|
187
|
-
? summarizeRelationEntries(
|
|
192
|
+
...(childIssues.length > 0
|
|
193
|
+
? summarizeRelationEntries(childIssues, { emptyText: "No child issues are currently tracked." })
|
|
188
194
|
: ["No child issues are currently tracked."]),
|
|
189
195
|
"",
|
|
190
196
|
...(unresolvedBlockers.length > 0
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -150,10 +150,8 @@ export class RunOrchestrator {
|
|
|
150
150
|
...(entry.blockerCurrentLinearState ? { stateName: entry.blockerCurrentLinearState } : {}),
|
|
151
151
|
...(entry.blockerCurrentLinearStateType ? { stateType: entry.blockerCurrentLinearStateType } : {}),
|
|
152
152
|
}));
|
|
153
|
-
const
|
|
154
|
-
.
|
|
155
|
-
.map((entry) => this.db.issues.getIssue(issue.projectId, entry.linearIssueId))
|
|
156
|
-
.filter((entry) => Boolean(entry))
|
|
153
|
+
const childIssues = this.db.issues
|
|
154
|
+
.listChildIssues(issue.projectId, issue.linearIssueId)
|
|
157
155
|
.map((entry) => ({
|
|
158
156
|
linearIssueId: entry.linearIssueId,
|
|
159
157
|
...(entry.issueKey ? { issueKey: entry.issueKey } : {}),
|
|
@@ -163,17 +161,17 @@ export class RunOrchestrator {
|
|
|
163
161
|
delegatedToPatchRelay: entry.delegatedToPatchRelay,
|
|
164
162
|
hasOpenPr: entry.prNumber !== undefined && entry.prState !== "closed" && entry.prState !== "merged",
|
|
165
163
|
}));
|
|
166
|
-
if (unresolvedBlockers.length === 0 &&
|
|
164
|
+
if (unresolvedBlockers.length === 0 && childIssues.length === 0) {
|
|
167
165
|
return {};
|
|
168
166
|
}
|
|
169
167
|
return {
|
|
170
168
|
...(unresolvedBlockers.length > 0 ? { unresolvedBlockers } : {}),
|
|
171
|
-
...(
|
|
169
|
+
...(childIssues.length > 0 ? { childIssues } : {}),
|
|
172
170
|
};
|
|
173
171
|
}
|
|
174
172
|
classifyTrackedIssue(issue) {
|
|
175
|
-
const
|
|
176
|
-
const classification = classifyIssue({ issue,
|
|
173
|
+
const childIssueCount = this.db.issues.listChildIssues(issue.projectId, issue.linearIssueId).length;
|
|
174
|
+
const classification = classifyIssue({ issue, childIssueCount });
|
|
177
175
|
if (issue.issueClass === classification.issueClass && issue.issueClassSource === classification.issueClassSource) {
|
|
178
176
|
return issue;
|
|
179
177
|
}
|
package/dist/run-reconciler.js
CHANGED
|
@@ -4,6 +4,7 @@ import { resolveAuthoritativeLinearStopState } from "./linear-workflow.js";
|
|
|
4
4
|
import { buildRunFailureActivity } from "./linear-session-reporting.js";
|
|
5
5
|
import { getThreadTurns } from "./codex-thread-utils.js";
|
|
6
6
|
import { resolveRecoverablePostRunState } from "./interrupted-run-recovery.js";
|
|
7
|
+
import { resolveEffectiveActiveRun } from "./effective-active-run.js";
|
|
7
8
|
export class RunReconciler {
|
|
8
9
|
db;
|
|
9
10
|
logger;
|
|
@@ -33,6 +34,19 @@ export class RunReconciler {
|
|
|
33
34
|
const { run, issue, recoveryLease } = params;
|
|
34
35
|
const acquiredRecoveryLease = recoveryLease === true;
|
|
35
36
|
let effectiveIssue = issue;
|
|
37
|
+
const effectiveActiveRun = resolveEffectiveActiveRun({
|
|
38
|
+
activeRun: issue.activeRunId === run.id ? run : undefined,
|
|
39
|
+
latestRun: run,
|
|
40
|
+
});
|
|
41
|
+
if (effectiveActiveRun?.id === run.id && issue.activeRunId !== run.id) {
|
|
42
|
+
effectiveIssue = this.withHeldLease(run.projectId, run.linearIssueId, () => this.db.issues.upsertIssue({
|
|
43
|
+
projectId: run.projectId,
|
|
44
|
+
linearIssueId: run.linearIssueId,
|
|
45
|
+
activeRunId: run.id,
|
|
46
|
+
...(run.threadId ? { threadId: run.threadId } : {}),
|
|
47
|
+
})) ?? effectiveIssue;
|
|
48
|
+
this.logger.info({ issueKey: effectiveIssue.issueKey, runId: run.id, runType: run.runType }, "Reattached detached active run during reconciliation");
|
|
49
|
+
}
|
|
36
50
|
if (!effectiveIssue.delegatedToPatchRelay) {
|
|
37
51
|
const authority = await this.confirmDelegationAuthorityBeforeRelease(run, effectiveIssue);
|
|
38
52
|
effectiveIssue = authority.issue;
|
|
@@ -2,6 +2,7 @@ import { parseGitHubFailureContext, summarizeGitHubFailureContext } from "./gith
|
|
|
2
2
|
import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
|
|
3
3
|
import { deriveIssueStatusNote } from "./status-note.js";
|
|
4
4
|
import { isIssueSessionReadyForExecution } from "./issue-session.js";
|
|
5
|
+
import { hasDetachedActiveLatestRun } from "./effective-active-run.js";
|
|
5
6
|
function shouldSuppressStatusNote(params) {
|
|
6
7
|
if (!params.activeRunType && params.sessionState !== "running")
|
|
7
8
|
return false;
|
|
@@ -82,6 +83,7 @@ export class TrackedIssueListQuery {
|
|
|
82
83
|
s.project_id, s.linear_issue_id, s.issue_key, i.title,
|
|
83
84
|
i.current_linear_state, i.factory_state, i.delegated_to_patchrelay, s.session_state, s.waiting_reason, s.summary_text, s.display_updated_at,
|
|
84
85
|
i.pending_run_type,
|
|
86
|
+
i.orchestration_settle_until,
|
|
85
87
|
i.pr_number, i.pr_state, i.pr_head_sha, i.pr_review_state, i.pr_check_status, i.last_blocking_review_head_sha,
|
|
86
88
|
i.last_github_ci_snapshot_json,
|
|
87
89
|
i.last_github_failure_source,
|
|
@@ -154,14 +156,28 @@ export class TrackedIssueListQuery {
|
|
|
154
156
|
const hasPendingSessionEvents = Number(row.pending_session_event_count ?? 0) > 0;
|
|
155
157
|
const hasPendingWake = hasPendingSessionEvents
|
|
156
158
|
|| this.db.issueSessions.peekIssueSessionWake(String(row.project_id), String(row.linear_issue_id)) !== undefined;
|
|
159
|
+
const detachedActiveRun = hasDetachedActiveLatestRun({
|
|
160
|
+
activeRunId: row.active_run_type !== null ? 1 : undefined,
|
|
161
|
+
latestRun: row.latest_run_status !== null
|
|
162
|
+
? { id: 0, status: String(row.latest_run_status) }
|
|
163
|
+
: undefined,
|
|
164
|
+
});
|
|
165
|
+
const effectiveActiveRunType = row.active_run_type !== null
|
|
166
|
+
? String(row.active_run_type)
|
|
167
|
+
: detachedActiveRun && row.latest_run_type !== null
|
|
168
|
+
? String(row.latest_run_type)
|
|
169
|
+
: undefined;
|
|
157
170
|
const readyForExecution = isIssueSessionReadyForExecution({
|
|
158
|
-
...(typeof row.session_state === "string"
|
|
171
|
+
...(typeof row.session_state === "string"
|
|
172
|
+
? { sessionState: detachedActiveRun ? "running" : String(row.session_state) }
|
|
173
|
+
: {}),
|
|
159
174
|
factoryState: String(row.factory_state ?? "delegated"),
|
|
160
175
|
...(row.delegated_to_patchrelay !== null ? { delegatedToPatchRelay: Number(row.delegated_to_patchrelay) !== 0 } : {}),
|
|
161
|
-
...(row.active_run_type !== null ? { activeRunId: 1 } : {}),
|
|
176
|
+
...((row.active_run_type !== null || detachedActiveRun) ? { activeRunId: 1 } : {}),
|
|
162
177
|
blockedByCount,
|
|
163
178
|
hasPendingWake,
|
|
164
179
|
hasLegacyPendingRun: row.pending_run_type !== null && row.pending_run_type !== undefined,
|
|
180
|
+
...(row.orchestration_settle_until !== null ? { orchestrationSettleUntil: String(row.orchestration_settle_until) } : {}),
|
|
165
181
|
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
166
182
|
...(row.pr_state !== null ? { prState: String(row.pr_state) } : {}),
|
|
167
183
|
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
@@ -175,12 +191,13 @@ export class TrackedIssueListQuery {
|
|
|
175
191
|
const sessionSummary = typeof row.summary_text === "string" && row.summary_text.trim().length > 0
|
|
176
192
|
? row.summary_text
|
|
177
193
|
: undefined;
|
|
178
|
-
const
|
|
194
|
+
const derivedWaitingReason = derivePatchRelayWaitingReason({
|
|
179
195
|
...(row.delegated_to_patchrelay !== null ? { delegatedToPatchRelay: Number(row.delegated_to_patchrelay) !== 0 } : {}),
|
|
180
|
-
...(row.active_run_type !== null ? {
|
|
196
|
+
...((row.active_run_type !== null || detachedActiveRun) ? { activeRunId: 1 } : {}),
|
|
181
197
|
blockedByKeys,
|
|
182
198
|
factoryState: String(row.factory_state ?? "delegated"),
|
|
183
199
|
...(row.pending_run_type !== null ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
200
|
+
...(row.orchestration_settle_until !== null ? { orchestrationSettleUntil: String(row.orchestration_settle_until) } : {}),
|
|
184
201
|
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
185
202
|
...(row.pr_state !== null ? { prState: String(row.pr_state) } : {}),
|
|
186
203
|
...(row.pr_head_sha !== null ? { prHeadSha: String(row.pr_head_sha) } : {}),
|
|
@@ -189,6 +206,7 @@ export class TrackedIssueListQuery {
|
|
|
189
206
|
...(row.last_blocking_review_head_sha !== null ? { lastBlockingReviewHeadSha: String(row.last_blocking_review_head_sha) } : {}),
|
|
190
207
|
...(row.last_github_failure_check_name !== null ? { latestFailureCheckName: String(row.last_github_failure_check_name) } : {}),
|
|
191
208
|
});
|
|
209
|
+
const waitingReason = detachedActiveRun ? derivedWaitingReason : sessionWaitingReason ?? derivedWaitingReason;
|
|
192
210
|
const latestRun = row.latest_run_type !== null && row.latest_run_status !== null
|
|
193
211
|
? {
|
|
194
212
|
id: 0,
|
|
@@ -219,29 +237,39 @@ export class TrackedIssueListQuery {
|
|
|
219
237
|
waitingReason,
|
|
220
238
|
}) ?? waitingReason;
|
|
221
239
|
const statusNoteForReturn = shouldSuppressStatusNote({
|
|
222
|
-
activeRunType:
|
|
223
|
-
sessionState: row.session_state,
|
|
240
|
+
activeRunType: effectiveActiveRunType,
|
|
241
|
+
sessionState: detachedActiveRun ? "running" : row.session_state,
|
|
224
242
|
statusNote: statusNoteCandidate,
|
|
225
243
|
})
|
|
226
244
|
? undefined
|
|
227
245
|
: statusNoteCandidate;
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
246
|
+
const activeCompletionCheckThreadId = row.active_run_type !== null
|
|
247
|
+
? row.active_completion_check_thread_id
|
|
248
|
+
: detachedActiveRun
|
|
249
|
+
? row.latest_run_completion_check_thread_id
|
|
250
|
+
: null;
|
|
251
|
+
const activeCompletionCheckOutcome = row.active_run_type !== null
|
|
252
|
+
? row.active_completion_check_outcome
|
|
253
|
+
: detachedActiveRun
|
|
254
|
+
? row.latest_run_completion_check_outcome
|
|
255
|
+
: null;
|
|
256
|
+
const completionCheckActive = typeof activeCompletionCheckThreadId === "string"
|
|
257
|
+
&& activeCompletionCheckThreadId.length > 0
|
|
258
|
+
&& activeCompletionCheckOutcome === null
|
|
259
|
+
&& effectiveActiveRunType !== undefined;
|
|
232
260
|
return {
|
|
233
261
|
...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
|
|
234
262
|
...(row.title !== null ? { title: String(row.title) } : {}),
|
|
235
263
|
...(statusNoteForReturn ? { statusNote: statusNoteForReturn } : {}),
|
|
236
264
|
projectId: String(row.project_id),
|
|
237
265
|
delegatedToPatchRelay: row.delegated_to_patchrelay === null ? true : Number(row.delegated_to_patchrelay) !== 0,
|
|
238
|
-
...(row.session_state !== null ? { sessionState: String(row.session_state) } : {}),
|
|
266
|
+
...(row.session_state !== null ? { sessionState: detachedActiveRun ? "running" : String(row.session_state) } : {}),
|
|
239
267
|
factoryState: String(row.factory_state ?? "delegated"),
|
|
240
268
|
blockedByCount,
|
|
241
269
|
blockedByKeys,
|
|
242
270
|
readyForExecution,
|
|
243
271
|
...(row.current_linear_state !== null ? { currentLinearState: String(row.current_linear_state) } : {}),
|
|
244
|
-
...(
|
|
272
|
+
...(effectiveActiveRunType ? { activeRunType: effectiveActiveRunType } : {}),
|
|
245
273
|
...(row.pending_run_type !== null ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
246
274
|
...(row.latest_run_type !== null ? { latestRunType: String(row.latest_run_type) } : {}),
|
|
247
275
|
...(row.latest_run_status !== null ? { latestRunStatus: String(row.latest_run_status) } : {}),
|
|
@@ -2,6 +2,7 @@ import { parseGitHubFailureContext } from "./github-failure-context.js";
|
|
|
2
2
|
import { isIssueSessionReadyForExecution } from "./issue-session.js";
|
|
3
3
|
import { deriveIssueStatusNote } from "./status-note.js";
|
|
4
4
|
import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
|
|
5
|
+
import { hasDetachedActiveLatestRun, resolveEffectiveActiveRun } from "./effective-active-run.js";
|
|
5
6
|
export function isResolvedLinearState(stateType, stateName) {
|
|
6
7
|
return stateType === "completed" || stateName?.trim().toLowerCase() === "done";
|
|
7
8
|
}
|
|
@@ -9,12 +10,21 @@ export function buildTrackedIssueRecord(params) {
|
|
|
9
10
|
const unresolvedBlockedBy = params.blockedBy.filter((entry) => !isResolvedLinearState(entry.blockerCurrentLinearStateType, entry.blockerCurrentLinearState));
|
|
10
11
|
const failureContext = parseGitHubFailureContext(params.issue.lastGitHubFailureContextJson);
|
|
11
12
|
const blockedByKeys = unresolvedBlockedBy.map((entry) => entry.blockerIssueKey ?? entry.blockerLinearIssueId);
|
|
13
|
+
const effectiveActiveRun = resolveEffectiveActiveRun({
|
|
14
|
+
activeRun: params.issue.activeRunId !== undefined && params.latestRun?.id === params.issue.activeRunId ? params.latestRun : undefined,
|
|
15
|
+
latestRun: params.latestRun,
|
|
16
|
+
});
|
|
17
|
+
const detachedActiveRun = hasDetachedActiveLatestRun({
|
|
18
|
+
activeRunId: params.issue.activeRunId,
|
|
19
|
+
latestRun: params.latestRun,
|
|
20
|
+
});
|
|
12
21
|
const waitingReason = derivePatchRelayWaitingReason({
|
|
13
22
|
delegatedToPatchRelay: params.issue.delegatedToPatchRelay,
|
|
14
|
-
...(
|
|
23
|
+
...(effectiveActiveRun ? { activeRunId: effectiveActiveRun.id } : {}),
|
|
15
24
|
blockedByKeys,
|
|
16
25
|
factoryState: params.issue.factoryState,
|
|
17
26
|
pendingRunType: params.issue.pendingRunType,
|
|
27
|
+
orchestrationSettleUntil: params.issue.orchestrationSettleUntil,
|
|
18
28
|
prNumber: params.issue.prNumber,
|
|
19
29
|
prState: params.issue.prState,
|
|
20
30
|
prHeadSha: params.issue.prHeadSha,
|
|
@@ -32,11 +42,9 @@ export function buildTrackedIssueRecord(params) {
|
|
|
32
42
|
blockedByKeys,
|
|
33
43
|
waitingReason,
|
|
34
44
|
});
|
|
35
|
-
const completionCheckActive = Boolean(
|
|
36
|
-
&&
|
|
37
|
-
&&
|
|
38
|
-
&& params.latestRun.completionCheckThreadId
|
|
39
|
-
&& !params.latestRun.completionCheckOutcome);
|
|
45
|
+
const completionCheckActive = Boolean(effectiveActiveRun?.status === "running"
|
|
46
|
+
&& effectiveActiveRun.completionCheckThreadId
|
|
47
|
+
&& !effectiveActiveRun.completionCheckOutcome);
|
|
40
48
|
return {
|
|
41
49
|
id: params.issue.id,
|
|
42
50
|
projectId: params.issue.projectId,
|
|
@@ -60,10 +68,11 @@ export function buildTrackedIssueRecord(params) {
|
|
|
60
68
|
sessionState: params.session?.sessionState,
|
|
61
69
|
factoryState: params.issue.factoryState,
|
|
62
70
|
delegatedToPatchRelay: params.issue.delegatedToPatchRelay,
|
|
63
|
-
activeRunId:
|
|
71
|
+
...(effectiveActiveRun ? { activeRunId: effectiveActiveRun.id } : {}),
|
|
64
72
|
blockedByCount: unresolvedBlockedBy.length,
|
|
65
73
|
hasPendingWake: params.hasPendingWake,
|
|
66
74
|
hasLegacyPendingRun: params.issue.pendingRunType !== undefined,
|
|
75
|
+
orchestrationSettleUntil: params.issue.orchestrationSettleUntil,
|
|
67
76
|
...(params.issue.prNumber !== undefined ? { prNumber: params.issue.prNumber } : {}),
|
|
68
77
|
...(params.issue.prState ? { prState: params.issue.prState } : {}),
|
|
69
78
|
...(params.issue.prReviewState ? { prReviewState: params.issue.prReviewState } : {}),
|
|
@@ -77,8 +86,9 @@ export function buildTrackedIssueRecord(params) {
|
|
|
77
86
|
...(failureContext?.summary ? { latestFailureSummary: failureContext.summary } : {}),
|
|
78
87
|
...(waitingReason ? { waitingReason } : {}),
|
|
79
88
|
...(completionCheckActive ? { completionCheckActive } : {}),
|
|
80
|
-
...(
|
|
89
|
+
...(effectiveActiveRun ? { activeRunId: effectiveActiveRun.id } : {}),
|
|
81
90
|
...(params.issue.agentSessionId ? { activeAgentSessionId: params.issue.agentSessionId } : {}),
|
|
91
|
+
...(detachedActiveRun && params.session?.sessionState ? { sessionState: "running" } : {}),
|
|
82
92
|
updatedAt: params.issue.updatedAt,
|
|
83
93
|
};
|
|
84
94
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildTrackedIssueRecord } from "./tracked-issue-projector.js";
|
|
2
2
|
import { deriveIssueSessionState, isIssueSessionReadyForExecution } from "./issue-session.js";
|
|
3
|
+
import { resolveEffectiveActiveRun } from "./effective-active-run.js";
|
|
3
4
|
export class TrackedIssueQuery {
|
|
4
5
|
issues;
|
|
5
6
|
issueSessions;
|
|
@@ -55,7 +56,10 @@ export class TrackedIssueQuery {
|
|
|
55
56
|
if (!issue)
|
|
56
57
|
return undefined;
|
|
57
58
|
const tracked = this.issueToTrackedIssue(issue);
|
|
58
|
-
const activeRun =
|
|
59
|
+
const activeRun = resolveEffectiveActiveRun({
|
|
60
|
+
activeRun: issue.activeRunId ? this.runs.getRunById(issue.activeRunId) : undefined,
|
|
61
|
+
latestRun: this.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId),
|
|
62
|
+
});
|
|
59
63
|
return {
|
|
60
64
|
issue: tracked,
|
|
61
65
|
...(activeRun ? { activeRun } : {}),
|
package/dist/waiting-reason.js
CHANGED
|
@@ -11,6 +11,7 @@ export const PATCHRELAY_WAITING_REASONS = {
|
|
|
11
11
|
sameHeadStillBlocked: "Requested changes still block the current head",
|
|
12
12
|
waitingForMergeStewardRepair: "Waiting to repair a merge-steward incident",
|
|
13
13
|
waitingForDownstreamAutomation: "PatchRelay work is done; waiting on downstream review/merge automation",
|
|
14
|
+
waitingForChildSettle: "Waiting briefly for child issues to settle before orchestration starts",
|
|
14
15
|
workComplete: "PatchRelay work is complete",
|
|
15
16
|
waitingForOperatorIntervention: "Waiting on operator intervention",
|
|
16
17
|
waitingForExternalReview: "Waiting on external review",
|
|
@@ -33,6 +34,12 @@ export function derivePatchRelayWaitingReason(params) {
|
|
|
33
34
|
if (params.activeRunId !== undefined) {
|
|
34
35
|
return PATCHRELAY_WAITING_REASONS.activeWork;
|
|
35
36
|
}
|
|
37
|
+
if (params.orchestrationSettleUntil) {
|
|
38
|
+
const settleAt = Date.parse(params.orchestrationSettleUntil);
|
|
39
|
+
if (Number.isFinite(settleAt) && settleAt > Date.now()) {
|
|
40
|
+
return PATCHRELAY_WAITING_REASONS.waitingForChildSettle;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
36
43
|
const blockedByKeys = (params.blockedByKeys ?? []).filter((value) => value.trim().length > 0);
|
|
37
44
|
if (blockedByKeys.length > 0) {
|
|
38
45
|
return `Blocked by ${blockedByKeys.join(", ")}`;
|
|
@@ -27,7 +27,7 @@ export class CommentWakeHandler {
|
|
|
27
27
|
return;
|
|
28
28
|
const issueClass = classifyIssue({
|
|
29
29
|
issue,
|
|
30
|
-
|
|
30
|
+
childIssueCount: this.db.issues.listChildIssues(project.id, normalized.issue.id).length,
|
|
31
31
|
}).issueClass;
|
|
32
32
|
const trimmedBody = normalized.comment.body.trim();
|
|
33
33
|
const installation = this.db.linearInstallations.getLinearInstallationForProject(project.id);
|
|
@@ -104,6 +104,9 @@ export function hasCompleteIssueContext(issue) {
|
|
|
104
104
|
export function mergeIssueMetadata(issue, liveIssue) {
|
|
105
105
|
return {
|
|
106
106
|
...issue,
|
|
107
|
+
...(issue.parentId ? {} : liveIssue.parentId ? { parentId: liveIssue.parentId } : {}),
|
|
108
|
+
...(issue.parentIdentifier ? {} : liveIssue.parentIdentifier ? { parentIdentifier: liveIssue.parentIdentifier } : {}),
|
|
109
|
+
...(issue.parentTitle ? {} : liveIssue.parentTitle ? { parentTitle: liveIssue.parentTitle } : {}),
|
|
107
110
|
...(issue.identifier ? {} : liveIssue.identifier ? { identifier: liveIssue.identifier } : {}),
|
|
108
111
|
...(issue.title ? {} : liveIssue.title ? { title: liveIssue.title } : {}),
|
|
109
112
|
...(issue.url ? {} : liveIssue.url ? { url: liveIssue.url } : {}),
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { classifyIssue } from "../issue-class.js";
|
|
2
|
+
import { computeOrchestrationSettleUntil, wakeOrchestrationParentsForChildEvent, } from "../orchestration-parent-wake.js";
|
|
1
3
|
import { triggerEventAllowed } from "../project-resolution.js";
|
|
2
4
|
import { resolveAwaitingInputReason } from "../awaiting-input-reason.js";
|
|
3
5
|
import { appendDelegationObservedEvent } from "../delegation-audit.js";
|
|
4
|
-
import { decideActiveRunRelease, decideAgentSession, decideRunIntent, decideUnDelegation, isTerminalDelegationState, mergeIssueMetadata, resolveReDelegationResume, } from "./decision-helpers.js";
|
|
6
|
+
import { decideActiveRunRelease, decideAgentSession, decideRunIntent, decideUnDelegation, isResolvedLinearState, isTerminalDelegationState, mergeIssueMetadata, resolveReDelegationResume, } from "./decision-helpers.js";
|
|
5
7
|
import { buildOperatorRetryEvent } from "../operator-retry-event.js";
|
|
6
8
|
import { resolveLinkedPullRequest } from "../linear-linked-pr-reconciliation.js";
|
|
7
9
|
import { readRemotePrState } from "../remote-pr-state.js";
|
|
@@ -70,6 +72,24 @@ export class DesiredStageRecorder {
|
|
|
70
72
|
terminal,
|
|
71
73
|
currentState: existingIssue?.factoryState,
|
|
72
74
|
});
|
|
75
|
+
const childIssueCount = this.db.issues.listChildIssues(params.project.id, normalizedIssue.id).length;
|
|
76
|
+
const classification = classifyIssue({
|
|
77
|
+
issue: {
|
|
78
|
+
issueClass: existingIssue?.issueClass,
|
|
79
|
+
issueClassSource: existingIssue?.issueClassSource,
|
|
80
|
+
title: hydratedIssue.title ?? existingIssue?.title,
|
|
81
|
+
description: hydratedIssue.description ?? existingIssue?.description,
|
|
82
|
+
parentLinearIssueId: hydratedIssue.parentId ?? existingIssue?.parentLinearIssueId,
|
|
83
|
+
},
|
|
84
|
+
childIssueCount,
|
|
85
|
+
});
|
|
86
|
+
const shouldEnterOrchestrationSettle = Boolean(delegated
|
|
87
|
+
&& desiredStage === "implementation"
|
|
88
|
+
&& classification.issueClass === "orchestration"
|
|
89
|
+
&& childIssueCount === 0
|
|
90
|
+
&& !existingIssue?.threadId
|
|
91
|
+
&& !activeRun
|
|
92
|
+
&& !terminal);
|
|
73
93
|
const runRelease = decideActiveRunRelease({
|
|
74
94
|
hasActiveRun: Boolean(activeRun),
|
|
75
95
|
terminal,
|
|
@@ -126,6 +146,10 @@ export class DesiredStageRecorder {
|
|
|
126
146
|
projectId: params.project.id,
|
|
127
147
|
linearIssueId: normalizedIssue.id,
|
|
128
148
|
...(hydratedIssue.identifier ? { issueKey: hydratedIssue.identifier } : {}),
|
|
149
|
+
...(hydratedIssue.parentId !== undefined ? { parentLinearIssueId: hydratedIssue.parentId ?? null } : {}),
|
|
150
|
+
...(hydratedIssue.parentIdentifier !== undefined ? { parentIssueKey: hydratedIssue.parentIdentifier ?? null } : {}),
|
|
151
|
+
issueClass: classification.issueClass,
|
|
152
|
+
issueClassSource: classification.issueClassSource,
|
|
129
153
|
...(hydratedIssue.title ? { title: hydratedIssue.title } : {}),
|
|
130
154
|
...(hydratedIssue.description ? { description: hydratedIssue.description } : {}),
|
|
131
155
|
...(hydratedIssue.url ? { url: hydratedIssue.url } : {}),
|
|
@@ -152,6 +176,7 @@ export class DesiredStageRecorder {
|
|
|
152
176
|
...(terminalRunRelease ? { factoryState: "done", pendingRunType: null, pendingRunContextJson: null } : {}),
|
|
153
177
|
...(blockerPausedImplementation ? { factoryState: "delegated" } : {}),
|
|
154
178
|
...(undelegation.factoryState ? { factoryState: undelegation.factoryState } : {}),
|
|
179
|
+
...(shouldEnterOrchestrationSettle ? { orchestrationSettleUntil: computeOrchestrationSettleUntil() } : {}),
|
|
155
180
|
});
|
|
156
181
|
if (effectiveRunRelease.release && activeRun && effectiveRunRelease.reason) {
|
|
157
182
|
this.db.runs.finishRun(activeRun.id, { status: "released", failureReason: effectiveRunRelease.reason });
|
|
@@ -166,6 +191,10 @@ export class DesiredStageRecorder {
|
|
|
166
191
|
...(hydratedIssue.identifier ? { issueKey: hydratedIssue.identifier } : {}),
|
|
167
192
|
}))
|
|
168
193
|
: this.db.transaction(commitIssueUpdate);
|
|
194
|
+
const previousParentIssueId = existingIssue?.parentLinearIssueId;
|
|
195
|
+
const currentParentIssueId = issue.parentLinearIssueId;
|
|
196
|
+
const wasResolved = isResolvedLinearState(existingIssue?.currentLinearStateType, existingIssue?.currentLinearState);
|
|
197
|
+
const isResolved = isResolvedLinearState(issue.currentLinearStateType, issue.currentLinearState);
|
|
169
198
|
if (undelegation.factoryState) {
|
|
170
199
|
if (activeRun?.threadId && activeRun.turnId) {
|
|
171
200
|
await params.stopActiveRun(activeRun, "STOP: The issue was un-delegated from PatchRelay. Stop working immediately and exit.");
|
|
@@ -213,6 +242,17 @@ export class DesiredStageRecorder {
|
|
|
213
242
|
...buildOperatorRetryEvent(issue, startupResume.pendingRunType, startupResume.source),
|
|
214
243
|
});
|
|
215
244
|
}
|
|
245
|
+
else if (shouldEnterOrchestrationSettle) {
|
|
246
|
+
this.feed?.publish({
|
|
247
|
+
level: "info",
|
|
248
|
+
kind: "stage",
|
|
249
|
+
issueKey: issue.issueKey,
|
|
250
|
+
projectId: params.project.id,
|
|
251
|
+
stage: issue.factoryState,
|
|
252
|
+
status: "settling_children",
|
|
253
|
+
summary: "Waiting briefly for child issues to settle before orchestration starts",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
216
256
|
else if (!startupResume.factoryState
|
|
217
257
|
&& !startupResume.pendingRunType
|
|
218
258
|
&&
|
|
@@ -232,6 +272,46 @@ export class DesiredStageRecorder {
|
|
|
232
272
|
dedupeKey: `delegated:${normalizedIssue.id}`,
|
|
233
273
|
});
|
|
234
274
|
}
|
|
275
|
+
if (previousParentIssueId && previousParentIssueId !== currentParentIssueId) {
|
|
276
|
+
wakeOrchestrationParentsForChildEvent({
|
|
277
|
+
db: this.db,
|
|
278
|
+
child: {
|
|
279
|
+
projectId: issue.projectId,
|
|
280
|
+
linearIssueId: issue.linearIssueId,
|
|
281
|
+
parentLinearIssueId: previousParentIssueId,
|
|
282
|
+
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
283
|
+
...(issue.title ? { title: issue.title } : {}),
|
|
284
|
+
factoryState: issue.factoryState,
|
|
285
|
+
...(issue.currentLinearState ? { currentLinearState: issue.currentLinearState } : {}),
|
|
286
|
+
...(issue.prNumber !== undefined ? { prNumber: issue.prNumber } : {}),
|
|
287
|
+
...(issue.prState ? { prState: issue.prState } : {}),
|
|
288
|
+
},
|
|
289
|
+
eventType: "child_changed",
|
|
290
|
+
changeKind: "detached",
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (currentParentIssueId) {
|
|
294
|
+
const changeKind = previousParentIssueId !== currentParentIssueId
|
|
295
|
+
? "attached"
|
|
296
|
+
: issue.currentLinearState?.trim().toLowerCase() === "duplicate"
|
|
297
|
+
? "duplicate"
|
|
298
|
+
: issue.currentLinearStateType === "canceled"
|
|
299
|
+
? "canceled"
|
|
300
|
+
: "updated";
|
|
301
|
+
const eventType = previousParentIssueId !== currentParentIssueId
|
|
302
|
+
? "child_changed"
|
|
303
|
+
: !wasResolved && isResolved
|
|
304
|
+
? "child_delivered"
|
|
305
|
+
: wasResolved && !isResolved
|
|
306
|
+
? "child_regressed"
|
|
307
|
+
: "child_changed";
|
|
308
|
+
wakeOrchestrationParentsForChildEvent({
|
|
309
|
+
db: this.db,
|
|
310
|
+
child: issue,
|
|
311
|
+
eventType,
|
|
312
|
+
changeKind,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
235
315
|
return {
|
|
236
316
|
issue: this.db.issueToTrackedIssue(issue),
|
|
237
317
|
wakeRunType: params.peekPendingSessionWakeRunType(params.project.id, normalizedIssue.id),
|
|
@@ -310,6 +390,11 @@ export class DesiredStageRecorder {
|
|
|
310
390
|
})),
|
|
311
391
|
});
|
|
312
392
|
}
|
|
393
|
+
this.db.issues.replaceIssueParentLink({
|
|
394
|
+
projectId,
|
|
395
|
+
childLinearIssueId: source.id,
|
|
396
|
+
parentLinearIssueId: source.parentId ?? null,
|
|
397
|
+
});
|
|
313
398
|
return { issue: source, hydration };
|
|
314
399
|
}
|
|
315
400
|
async resolveLinkedPrAdoption(params) {
|