@vercel/factory 0.0.17 → 0.0.18
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/CHANGELOG.md +46 -0
- package/dist/api-contracts.d.mts +665 -715
- package/dist/api-contracts.mjs +91 -4
- package/dist/api-contracts.mjs.map +1 -1
- package/dist/api-task-graph.mjs +14 -5
- package/dist/api-task-graph.mjs.map +1 -1
- package/dist/api.d.mts +58 -11
- package/dist/api.mjs +715 -399
- package/dist/api.mjs.map +1 -1
- package/dist/approval-access.d.mts +1 -1
- package/dist/approval-contracts.d.mts +3 -4
- package/dist/approval-contracts.mjs +1 -2
- package/dist/approval-contracts.mjs.map +1 -1
- package/dist/blob/index.d.mts +1 -1
- package/dist/budget.mjs +20 -3
- package/dist/budget.mjs.map +1 -1
- package/dist/change-verification/brief.d.mts +1 -1
- package/dist/change-verification/dispatch.mjs +8 -1
- package/dist/change-verification/dispatch.mjs.map +1 -1
- package/dist/changes/eve-record-change.mjs +4 -1
- package/dist/changes/eve-record-change.mjs.map +1 -1
- package/dist/client-stream.d.mts +19 -2
- package/dist/client-stream.mjs +115 -53
- package/dist/client-stream.mjs.map +1 -1
- package/dist/client-transcript.d.mts +25 -2
- package/dist/client-transcript.mjs +95 -13
- package/dist/client-transcript.mjs.map +1 -1
- package/dist/client.d.mts +145 -19
- package/dist/client.mjs +89 -16
- package/dist/client.mjs.map +1 -1
- package/dist/code-review/contracts.d.mts +1 -0
- package/dist/code-review/eve-post-review.d.mts +4 -2
- package/dist/code-review/eve-post-review.mjs +8 -0
- package/dist/code-review/eve-post-review.mjs.map +1 -1
- package/dist/code-review/github-reporter.d.mts +6 -2
- package/dist/code-review/github-reporter.mjs +86 -35
- package/dist/code-review/github-reporter.mjs.map +1 -1
- package/dist/code-review/preparation.d.mts +2 -1
- package/dist/code-review/preparation.mjs +2 -1
- package/dist/code-review/preparation.mjs.map +1 -1
- package/dist/code-review/stall.mjs +11 -2
- package/dist/code-review/stall.mjs.map +1 -1
- package/dist/connectors/github.d.mts +1 -1
- package/dist/connectors/index.d.mts +1 -1
- package/dist/connectors/slack.d.mts +1 -1
- package/dist/delivery-metrics/metrics.d.mts +1 -1
- package/dist/dispatch.mjs +3 -3
- package/dist/dispatch.mjs.map +1 -1
- package/dist/eve/index.d.mts +1 -1
- package/dist/eve/index.mjs +8 -1
- package/dist/eve/index.mjs.map +1 -1
- package/dist/eve/task-execution.d.mts +4 -4
- package/dist/eve/transcript.d.mts +3 -1
- package/dist/eve/transcript.mjs.map +1 -1
- package/dist/finding-remediation/admission.d.mts +1 -1
- package/dist/finding-remediation/brief.d.mts +1 -1
- package/dist/github-app.d.mts +1 -1
- package/dist/github-publication.d.mts +1 -1
- package/dist/github.d.mts +9 -1
- package/dist/github.mjs +29 -11
- package/dist/github.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/intake-contracts.d.mts +1 -0
- package/dist/intake-contracts.mjs +2 -1
- package/dist/intake-contracts.mjs.map +1 -1
- package/dist/integrations/github-issues.d.mts +53 -0
- package/dist/integrations/github-issues.mjs +170 -0
- package/dist/integrations/github-issues.mjs.map +1 -0
- package/dist/integrations/github.d.mts +2 -1
- package/dist/integrations/github.mjs +2 -1
- package/dist/multi-repository-changes/coordination.mjs +1 -1
- package/dist/planning/reconcile.mjs +15 -2
- package/dist/planning/reconcile.mjs.map +1 -1
- package/dist/postgres/index.d.mts +2 -1
- package/dist/postgres/index.mjs +11 -5
- package/dist/postgres/index.mjs.map +1 -1
- package/dist/postgres/indexed-codec.mjs +137 -0
- package/dist/postgres/indexed-codec.mjs.map +1 -0
- package/dist/postgres/indexed-schema.mjs +99 -0
- package/dist/postgres/indexed-schema.mjs.map +1 -0
- package/dist/postgres/indexed.d.mts +28 -0
- package/dist/postgres/indexed.mjs +277 -0
- package/dist/postgres/indexed.mjs.map +1 -0
- package/dist/postgres/record-predicates.mjs +23 -0
- package/dist/postgres/record-predicates.mjs.map +1 -0
- package/dist/presets/software-development/deepsec-brief.d.mts +1 -1
- package/dist/presets/software-development/dispatch.mjs +1 -1
- package/dist/presets/software-development/dispatch.mjs.map +1 -1
- package/dist/presets/software-development/recovery.mjs +9 -2
- package/dist/presets/software-development/recovery.mjs.map +1 -1
- package/dist/pull-requests/eve-fetch.d.mts +4 -1
- package/dist/pull-requests/eve-fetch.mjs +9 -0
- package/dist/pull-requests/eve-fetch.mjs.map +1 -1
- package/dist/pull-requests/github-publisher.d.mts +1 -1
- package/dist/pull-requests/github-publisher.mjs +19 -7
- package/dist/pull-requests/github-publisher.mjs.map +1 -1
- package/dist/repositories.mjs +20 -13
- package/dist/repositories.mjs.map +1 -1
- package/dist/sandbox/execution-run-ledger.d.mts +2 -2
- package/dist/sandbox/execution-run-ledger.mjs.map +1 -1
- package/dist/sandbox/execution.d.mts +2 -2
- package/dist/sandbox/execution.mjs.map +1 -1
- package/dist/schema/catalog.mjs +1 -1
- package/dist/schema/change.d.mts +1 -1
- package/dist/schema/communication.d.mts +2 -2
- package/dist/schema/factory-config.d.mts +1 -1
- package/dist/schema/outbox.d.mts +6 -6
- package/dist/schema/session-summary.d.mts +218 -0
- package/dist/schema/session-summary.mjs +68 -0
- package/dist/schema/session-summary.mjs.map +1 -0
- package/dist/schema/session.d.mts +39 -59
- package/dist/schema/session.mjs +6 -6
- package/dist/schema/session.mjs.map +1 -1
- package/dist/schema/task-graph.d.mts +24 -24
- package/dist/schema/transcript.d.mts +141 -3
- package/dist/schema/transcript.mjs +19 -3
- package/dist/schema/transcript.mjs.map +1 -1
- package/dist/schema/transitions.d.mts +1 -1
- package/dist/schema/transitions.mjs +1 -1
- package/dist/schema/transitions.mjs.map +1 -1
- package/dist/signals.d.mts +1 -1
- package/dist/stall.mjs +10 -2
- package/dist/stall.mjs.map +1 -1
- package/dist/storage.d.mts +10 -1
- package/dist/storage.mjs +7 -1
- package/dist/store/approvals.d.mts +14 -0
- package/dist/store/approvals.mjs +78 -0
- package/dist/store/approvals.mjs.map +1 -0
- package/dist/store/conversations.d.mts +59 -0
- package/dist/store/conversations.mjs +549 -0
- package/dist/store/conversations.mjs.map +1 -0
- package/dist/store/driver.d.mts +1 -1
- package/dist/store/driver.mjs.map +1 -1
- package/dist/store/engine.d.mts +38 -16
- package/dist/store/engine.mjs +229 -106
- package/dist/store/engine.mjs.map +1 -1
- package/dist/store/indexed-engine.d.mts +57 -0
- package/dist/store/indexed-engine.mjs +155 -0
- package/dist/store/indexed-engine.mjs.map +1 -0
- package/dist/store/indexed-memory.d.mts +7 -0
- package/dist/store/indexed-memory.mjs +200 -0
- package/dist/store/indexed-memory.mjs.map +1 -0
- package/dist/store/indexed-projections.d.mts +280 -0
- package/dist/store/indexed-projections.mjs +481 -0
- package/dist/store/indexed-projections.mjs.map +1 -0
- package/dist/store/indexed.d.mts +101 -0
- package/dist/store/indexed.mjs +19 -0
- package/dist/store/indexed.mjs.map +1 -0
- package/dist/store/iterate.d.mts +9 -0
- package/dist/store/iterate.mjs +23 -0
- package/dist/store/iterate.mjs.map +1 -0
- package/dist/store/query-scope.mjs +62 -0
- package/dist/store/query-scope.mjs.map +1 -0
- package/dist/store/query.d.mts +32 -0
- package/dist/store/query.mjs +103 -0
- package/dist/store/query.mjs.map +1 -0
- package/dist/store/session-history.mjs +24 -0
- package/dist/store/session-history.mjs.map +1 -0
- package/dist/store/slack-pr-notifications.d.mts +2 -2
- package/dist/store/slack-pr-notifications.mjs.map +1 -1
- package/dist/store/state.d.mts +26 -0
- package/dist/store/state.mjs +95 -0
- package/dist/store/state.mjs.map +1 -0
- package/dist/store/task-effects.d.mts +1 -1
- package/dist/store/task-graphs.d.mts +1 -1
- package/dist/store/task-messages.d.mts +1 -1
- package/dist/store/task-work.d.mts +1 -1
- package/dist/store/task-work.mjs +1 -1
- package/dist/store/transcripts.mjs +308 -0
- package/dist/store/transcripts.mjs.map +1 -0
- package/dist/sweep.d.mts +1 -1
- package/dist/sweep.mjs +12 -7
- package/dist/sweep.mjs.map +1 -1
- package/dist/task-communication.mjs +15 -1
- package/dist/task-communication.mjs.map +1 -1
- package/dist/task-protocols.d.mts +3 -3
- package/dist/tasks.d.mts +1 -1
- package/dist/vercel.d.mts +1 -1
- package/dist/work-triage/find-existing-work.d.mts +1 -1
- package/dist/work-triage/find-existing-work.mjs +12 -2
- package/dist/work-triage/find-existing-work.mjs.map +1 -1
- package/docs/index.md +4 -0
- package/docs/recipes/persistence-recovery.md +15 -9
- package/docs/recipes/postgres-indexed-engine.md +108 -0
- package/docs/recipes/postgres-indexed-storage.md +145 -0
- package/package.json +3 -1
|
@@ -6,10 +6,16 @@ const stateSuffix = " -->";
|
|
|
6
6
|
const terminalReviewStates = /* @__PURE__ */ new Set(["cancelled", "failed"]);
|
|
7
7
|
const reviewCheckBatchSize = 20;
|
|
8
8
|
const reviewCheckPageIntervalMs = 3e5;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
function reviewCheckEffectKey(attempt) {
|
|
10
|
+
if (!Number.isSafeInteger(attempt) || attempt < 1) throw new Error("Review checks require a positive attempt");
|
|
11
|
+
return {
|
|
12
|
+
namespace: "software",
|
|
13
|
+
name: attempt === 1 ? "github_review_check" : `github_review_check_${attempt}`
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function reviewCheckExternalId(taskId, attempt) {
|
|
17
|
+
return attempt === 1 ? taskId : `${taskId}:${attempt}`;
|
|
18
|
+
}
|
|
13
19
|
function recordedCheckRunId(effect, headSha) {
|
|
14
20
|
if (effect?.reference === void 0) return void 0;
|
|
15
21
|
const { provider, resourceId } = effect.reference;
|
|
@@ -24,11 +30,20 @@ function createGitHubReviewReporter(options) {
|
|
|
24
30
|
const summaryHeading = options.summaryHeading ?? "Factory code review";
|
|
25
31
|
const now = () => (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
26
32
|
async function startReviewCheck(input) {
|
|
33
|
+
const key = reviewCheckEffectKey(input.attempt);
|
|
34
|
+
if (input.attempt > 1) {
|
|
35
|
+
const task = await options.stores.tasks.get(input.taskId);
|
|
36
|
+
if (!task) throw new Error("Review Task is unavailable");
|
|
37
|
+
for (const attempt of /* @__PURE__ */ new Set([1, input.attempt - 1])) await supersedeReviewCheck({
|
|
38
|
+
...task,
|
|
39
|
+
attempt
|
|
40
|
+
});
|
|
41
|
+
}
|
|
27
42
|
const token = await options.tokenForRepository(input.repository);
|
|
28
43
|
if (input.reuseExisting) {
|
|
29
44
|
const effect = await options.stores.taskEffects.get({
|
|
30
45
|
taskId: input.taskId,
|
|
31
|
-
key
|
|
46
|
+
key
|
|
32
47
|
});
|
|
33
48
|
if (recordedCheckRunId(effect, input.headSha) !== void 0 || effect?.resolvedAt !== void 0) return;
|
|
34
49
|
const existing = (await listCheckRunsForRef({
|
|
@@ -37,9 +52,9 @@ function createGitHubReviewReporter(options) {
|
|
|
37
52
|
fetch: options.fetch,
|
|
38
53
|
ref: input.headSha,
|
|
39
54
|
checkName
|
|
40
|
-
})).find((check) => check.externalId === input.taskId);
|
|
55
|
+
})).find((check) => check.externalId === reviewCheckExternalId(input.taskId, input.attempt));
|
|
41
56
|
if (existing !== void 0) {
|
|
42
|
-
await recordReviewCheck(input.taskId, existing.id, input.headSha);
|
|
57
|
+
await recordReviewCheck(input.taskId, input.attempt, existing.id, input.headSha);
|
|
43
58
|
return;
|
|
44
59
|
}
|
|
45
60
|
}
|
|
@@ -50,7 +65,7 @@ function createGitHubReviewReporter(options) {
|
|
|
50
65
|
name: checkName,
|
|
51
66
|
headSha: input.headSha,
|
|
52
67
|
status: "in_progress",
|
|
53
|
-
externalId: input.taskId,
|
|
68
|
+
externalId: reviewCheckExternalId(input.taskId, input.attempt),
|
|
54
69
|
detailsUrl: input.pullRequestUrl,
|
|
55
70
|
startedAt: now(),
|
|
56
71
|
output: {
|
|
@@ -58,17 +73,17 @@ function createGitHubReviewReporter(options) {
|
|
|
58
73
|
summary: "The Factory code reviewer is inspecting this commit."
|
|
59
74
|
}
|
|
60
75
|
});
|
|
61
|
-
await recordReviewCheck(input.taskId, checkRun.id, input.headSha);
|
|
76
|
+
await recordReviewCheck(input.taskId, input.attempt, checkRun.id, input.headSha);
|
|
62
77
|
}
|
|
63
78
|
async function completeReviewCheck(review) {
|
|
64
79
|
const token = await options.tokenForRepository(review.repository);
|
|
65
80
|
let checkRunId = recordedCheckRunId(await options.stores.taskEffects.get({
|
|
66
81
|
taskId: review.taskId,
|
|
67
|
-
key: reviewCheckEffectKey
|
|
82
|
+
key: reviewCheckEffectKey(review.attempt)
|
|
68
83
|
}), review.headSha);
|
|
69
84
|
if (checkRunId === void 0) {
|
|
70
85
|
checkRunId = (await findReviewCheck(review, token)).id;
|
|
71
|
-
await recordReviewCheck(review.taskId, checkRunId, review.headSha);
|
|
86
|
+
await recordReviewCheck(review.taskId, review.attempt, checkRunId, review.headSha);
|
|
72
87
|
}
|
|
73
88
|
await updateCheckRun({
|
|
74
89
|
repository: review.repository,
|
|
@@ -93,7 +108,7 @@ function createGitHubReviewReporter(options) {
|
|
|
93
108
|
});
|
|
94
109
|
await options.stores.taskEffects.resolve({
|
|
95
110
|
taskId: review.taskId,
|
|
96
|
-
key: reviewCheckEffectKey
|
|
111
|
+
key: reviewCheckEffectKey(review.attempt)
|
|
97
112
|
});
|
|
98
113
|
if (typeof options.publishSummary === "function" ? await options.publishSummary(review) : options.publishSummary !== false) await updateReviewSummary(review, token);
|
|
99
114
|
}
|
|
@@ -105,35 +120,49 @@ function createGitHubReviewReporter(options) {
|
|
|
105
120
|
const candidates = tasks.filter((task) => task.work.route?.id === "code-review" && terminalReviewStates.has(task.state)).sort((left, right) => left.id.localeCompare(right.id));
|
|
106
121
|
const candidatePage = reconcileOptions.candidatePage ?? Math.floor(reconciliationNow().getTime() / reviewCheckPageIntervalMs);
|
|
107
122
|
if (!Number.isSafeInteger(candidatePage) || candidatePage < 0) throw new Error("Review-check candidate page must be a nonnegative safe integer");
|
|
108
|
-
const
|
|
123
|
+
const pageCount = Math.max(1, Math.ceil(candidates.length / batchSize));
|
|
124
|
+
const pageStart = candidatePage % pageCount * batchSize;
|
|
109
125
|
const page = candidates.slice(pageStart, pageStart + batchSize);
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
const attemptPage = Math.floor(candidatePage / pageCount);
|
|
127
|
+
const pending = (await Promise.all(page.map(async (task) => {
|
|
128
|
+
const attempts = /* @__PURE__ */ new Set([task.attempt, 1]);
|
|
129
|
+
if (task.attempt > 2) attempts.add(task.attempt - 1 - attemptPage % (task.attempt - 2));
|
|
130
|
+
const effects = await Promise.all([...attempts].map(async (attempt) => {
|
|
131
|
+
const key = reviewCheckEffectKey(attempt);
|
|
132
|
+
return {
|
|
133
|
+
task,
|
|
134
|
+
attempt,
|
|
135
|
+
key,
|
|
136
|
+
effect: await options.stores.taskEffects.get({
|
|
137
|
+
taskId: task.id,
|
|
138
|
+
key
|
|
139
|
+
})
|
|
140
|
+
};
|
|
141
|
+
}));
|
|
142
|
+
const recorded = effects.filter(({ effect }) => effect?.reference !== void 0 && effect.resolvedAt === void 0);
|
|
143
|
+
if (recorded.length > 0) return recorded;
|
|
144
|
+
return effects.filter(({ attempt, effect }) => attempt === task.attempt && effect?.resolvedAt === void 0);
|
|
145
|
+
}))).flat();
|
|
146
|
+
const concluded = /* @__PURE__ */ new Set();
|
|
147
|
+
const stamped = /* @__PURE__ */ new Set();
|
|
148
|
+
for (const { task, key, effect } of pending) try {
|
|
149
|
+
if (await concludeAbandonedCheck(task, effect, reconciliationNow)) concluded.add(task.id);
|
|
121
150
|
await options.stores.taskEffects.resolve({
|
|
122
151
|
taskId: task.id,
|
|
123
|
-
key
|
|
152
|
+
key,
|
|
124
153
|
at: reconciliationNow().toISOString()
|
|
125
154
|
});
|
|
126
|
-
stamped.
|
|
155
|
+
stamped.add(task.id);
|
|
127
156
|
} catch {}
|
|
128
157
|
return {
|
|
129
|
-
concluded,
|
|
130
|
-
stamped
|
|
158
|
+
concluded: [...concluded],
|
|
159
|
+
stamped: [...stamped]
|
|
131
160
|
};
|
|
132
161
|
}
|
|
133
|
-
async function recordReviewCheck(taskId, checkRunId, headSha) {
|
|
162
|
+
async function recordReviewCheck(taskId, attempt, checkRunId, headSha) {
|
|
134
163
|
await options.stores.taskEffects.record({
|
|
135
164
|
taskId,
|
|
136
|
-
key: reviewCheckEffectKey,
|
|
165
|
+
key: reviewCheckEffectKey(attempt),
|
|
137
166
|
reference: {
|
|
138
167
|
provider: "github",
|
|
139
168
|
resourceId: String(checkRunId),
|
|
@@ -141,6 +170,27 @@ function createGitHubReviewReporter(options) {
|
|
|
141
170
|
}
|
|
142
171
|
});
|
|
143
172
|
}
|
|
173
|
+
async function supersedeReviewCheck(task) {
|
|
174
|
+
const key = reviewCheckEffectKey(task.attempt);
|
|
175
|
+
const effect = await options.stores.taskEffects.get({
|
|
176
|
+
taskId: task.id,
|
|
177
|
+
key
|
|
178
|
+
});
|
|
179
|
+
if (!effect?.reference || effect.resolvedAt) return;
|
|
180
|
+
if (!await concludeAbandonedCheck(task, effect, options.now ?? (() => /* @__PURE__ */ new Date()), true)) throw new Error("Obsolete review check could not be closed");
|
|
181
|
+
await options.stores.taskEffects.resolve({
|
|
182
|
+
taskId: task.id,
|
|
183
|
+
key
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async function readReviewHead(task) {
|
|
187
|
+
const effect = await options.stores.taskEffects.get({
|
|
188
|
+
taskId: task.id,
|
|
189
|
+
key: reviewCheckEffectKey(task.attempt)
|
|
190
|
+
});
|
|
191
|
+
recordedCheckRunId(effect);
|
|
192
|
+
return effect?.reference?.subject ?? null;
|
|
193
|
+
}
|
|
144
194
|
async function updateReviewSummary(review, token) {
|
|
145
195
|
const summary = findSummaryComment(await listPullRequestComments({
|
|
146
196
|
repository: review.repository,
|
|
@@ -189,11 +239,11 @@ function createGitHubReviewReporter(options) {
|
|
|
189
239
|
fetch: options.fetch,
|
|
190
240
|
ref: review.headSha,
|
|
191
241
|
checkName
|
|
192
|
-
})).find((candidate) => candidate.externalId === review.taskId);
|
|
242
|
+
})).find((candidate) => candidate.externalId === reviewCheckExternalId(review.taskId, review.attempt));
|
|
193
243
|
if (checkRun === void 0) throw new Error(`could not find ${checkName} check for task ${review.taskId} on ${review.headSha}`);
|
|
194
244
|
return checkRun;
|
|
195
245
|
}
|
|
196
|
-
async function concludeAbandonedCheck(task, effect, reconciliationNow) {
|
|
246
|
+
async function concludeAbandonedCheck(task, effect, reconciliationNow, cancelled = task.state === "cancelled") {
|
|
197
247
|
const change = task.changeId === void 0 ? null : await options.stores.changes.get(task.changeId);
|
|
198
248
|
if (change === null) return false;
|
|
199
249
|
const repository = await options.stores.repositories.get(change.repositoryId);
|
|
@@ -201,7 +251,6 @@ function createGitHubReviewReporter(options) {
|
|
|
201
251
|
const token = await options.tokenForRepository(repository.slug);
|
|
202
252
|
const checkRunId = recordedCheckRunId(effect) ?? await discoverCheckRun(repository.slug, token, task, change.prNumber);
|
|
203
253
|
if (checkRunId === void 0) return false;
|
|
204
|
-
const cancelled = task.state === "cancelled";
|
|
205
254
|
await updateCheckRun({
|
|
206
255
|
repository: repository.slug,
|
|
207
256
|
token,
|
|
@@ -212,7 +261,7 @@ function createGitHubReviewReporter(options) {
|
|
|
212
261
|
completedAt: reconciliationNow().toISOString(),
|
|
213
262
|
output: {
|
|
214
263
|
title: cancelled ? "Review cancelled" : "Review did not complete",
|
|
215
|
-
summary: `
|
|
264
|
+
summary: `This review attempt ended as ${task.state} without recording a verdict.`
|
|
216
265
|
}
|
|
217
266
|
});
|
|
218
267
|
return true;
|
|
@@ -230,11 +279,13 @@ function createGitHubReviewReporter(options) {
|
|
|
230
279
|
fetch: options.fetch,
|
|
231
280
|
ref: head.sha,
|
|
232
281
|
checkName
|
|
233
|
-
})).find((candidate) => candidate.externalId === task.id && candidate.status !== "completed")?.id;
|
|
282
|
+
})).find((candidate) => candidate.externalId === reviewCheckExternalId(task.id, task.attempt) && candidate.status !== "completed")?.id;
|
|
234
283
|
}
|
|
235
284
|
return Object.freeze({
|
|
236
285
|
startReviewCheck,
|
|
237
286
|
completeReviewCheck,
|
|
287
|
+
supersedeReviewCheck,
|
|
288
|
+
readReviewHead,
|
|
238
289
|
reconcileReviewChecks
|
|
239
290
|
});
|
|
240
291
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"github-reporter.mjs","names":[],"sources":["../../src/code-review/github-reporter.ts"],"sourcesContent":["import {\n createCheckRun,\n createPullRequestComment,\n listCheckRunsForRef,\n listPullRequestComments,\n readPullRequestHead,\n updateCheckRun,\n updatePullRequestComment,\n type GitHubIssueComment,\n} from \"../github\";\nimport type { TaskId } from \"../schema/id\";\nimport type { Task } from \"../schema/task\";\nimport type { TaskEffect } from \"../schema/task-effect\";\nimport type { FactoryStores } from \"../store/engine\";\nimport type { DateClock } from \"../schema/common\";\nimport type { RepositorySlug } from \"../schema/repository\";\nimport type { CompletedReview, CompletedReviewFinding } from \"./contracts\";\nimport type { StartReviewCheckInput } from \"./preparation\";\n\n/** Storage, credentials, and presentation settings for GitHub review reporting. */\nexport interface GitHubReviewReporterOptions {\n stores: Pick<FactoryStores, \"taskEffects\" | \"changes\" | \"repositories\">;\n tokenForRepository(repositorySlug: RepositorySlug): Promise<string>;\n fetch?: typeof globalThis.fetch;\n checkName?: string;\n summaryHeading?: string;\n /** Whether to maintain a separate finding-summary comment. Defaults to true. */\n publishSummary?: boolean | ((review: CompletedReview) => boolean | Promise<boolean>);\n now?: DateClock;\n}\n\n/** Check-run and pull-request summary lifecycle for supplied code reviews. */\nexport interface GitHubReviewReporter {\n startReviewCheck(input: StartReviewCheckInput): Promise<void>;\n completeReviewCheck(review: CompletedReview): Promise<void>;\n reconcileReviewChecks(input: ReconcileReviewChecksInput): Promise<ReconcileReviewChecksResult>;\n}\n\n/** Bounded rotating-page controls for abandoned review-check recovery. */\nexport interface ReconcileReviewChecksOptions {\n batchSize?: number;\n /** Stable page selector; defaults to the current five-minute recovery window. */\n candidatePage?: number;\n now?: DateClock;\n}\n\n/** Review Tasks and bounded recovery controls for one reconciliation pass. */\nexport interface ReconcileReviewChecksInput extends ReconcileReviewChecksOptions {\n tasks: readonly Task[];\n}\n\n/** Review Tasks whose check effects were concluded or durably resolved in one pass. */\nexport interface ReconcileReviewChecksResult {\n concluded: readonly TaskId[];\n stamped: readonly TaskId[];\n}\n\ninterface ReviewFinding {\n fingerprint: string;\n title: string;\n path: string;\n line: number;\n body: string;\n}\n\ninterface ReviewSummaryState {\n active: ReviewFinding[];\n}\n\nconst summaryMarker = \"<!-- factory-code-review-summary -->\";\nconst statePrefix = \"<!-- factory-code-review-state:v1:\";\nconst stateSuffix = \" -->\";\nconst terminalReviewStates: ReadonlySet<Task[\"state\"]> = new Set([\"cancelled\", \"failed\"]);\nconst reviewCheckBatchSize = 20;\nconst reviewCheckPageIntervalMs = 300_000;\nconst reviewCheckEffectKey = {\n namespace: \"software\",\n name: \"github_review_check\",\n} as const;\n\nfunction recordedCheckRunId(effect: TaskEffect | null, headSha?: string): number | undefined {\n if (effect?.reference === undefined) return undefined;\n const { provider, resourceId } = effect.reference;\n const checkRunId = Number(resourceId);\n if (provider !== \"github\" || !Number.isSafeInteger(checkRunId) || checkRunId <= 0) {\n throw new Error(\n \"Stored software review-check effect has an invalid GitHub check-run reference\",\n );\n }\n if (headSha !== undefined && effect.reference.subject !== headSha) {\n throw new Error(\n `Review task ${effect.taskId} has a check for another commit. Request a new review for ${headSha}.`,\n );\n }\n return checkRunId;\n}\n\n/** Create the supplied GitHub check-run and pull-request summary reporter for code reviews. */\nexport function createGitHubReviewReporter(\n options: GitHubReviewReporterOptions,\n): GitHubReviewReporter {\n const checkName = options.checkName ?? \"Factory code review\";\n const summaryHeading = options.summaryHeading ?? \"Factory code review\";\n const now = () => (options.now ?? (() => new Date()))().toISOString();\n\n async function startReviewCheck(input: StartReviewCheckInput): Promise<void> {\n const token = await options.tokenForRepository(input.repository);\n if (input.reuseExisting) {\n const effect = await options.stores.taskEffects.get({\n taskId: input.taskId,\n key: reviewCheckEffectKey,\n });\n if (\n recordedCheckRunId(effect, input.headSha) !== undefined ||\n effect?.resolvedAt !== undefined\n )\n return;\n const checks = await listCheckRunsForRef({\n repository: input.repository,\n token,\n fetch: options.fetch,\n ref: input.headSha,\n checkName,\n });\n const existing = checks.find((check) => check.externalId === input.taskId);\n if (existing !== undefined) {\n await recordReviewCheck(input.taskId, existing.id, input.headSha);\n return;\n }\n }\n const checkRun = await createCheckRun({\n repository: input.repository,\n token,\n fetch: options.fetch,\n name: checkName,\n headSha: input.headSha,\n status: \"in_progress\",\n externalId: input.taskId,\n detailsUrl: input.pullRequestUrl,\n startedAt: now(),\n output: {\n title: \"Review in progress\",\n summary: \"The Factory code reviewer is inspecting this commit.\",\n },\n });\n await recordReviewCheck(input.taskId, checkRun.id, input.headSha);\n }\n\n async function completeReviewCheck(review: CompletedReview): Promise<void> {\n const token = await options.tokenForRepository(review.repository);\n const effect = await options.stores.taskEffects.get({\n taskId: review.taskId,\n key: reviewCheckEffectKey,\n });\n let checkRunId = recordedCheckRunId(effect, review.headSha);\n if (checkRunId === undefined) {\n checkRunId = (await findReviewCheck(review, token)).id;\n await recordReviewCheck(review.taskId, checkRunId, review.headSha);\n }\n await updateCheckRun({\n repository: review.repository,\n token,\n fetch: options.fetch,\n checkRunId,\n status: \"completed\",\n conclusion: review.verdict === \"request_changes\" ? \"failure\" : \"success\",\n completedAt: now(),\n output: {\n title:\n review.verdict === \"request_changes\" ? \"Review found blocking issues\" : \"Review complete\",\n summary: review.summary,\n annotations: review.comments.map((comment) => ({\n path: comment.path,\n startLine: comment.startLine ?? comment.line,\n endLine: comment.line,\n annotationLevel: review.verdict === \"request_changes\" ? \"failure\" : \"notice\",\n title: comment.title,\n message: comment.body,\n })),\n },\n });\n await options.stores.taskEffects.resolve({\n taskId: review.taskId,\n key: reviewCheckEffectKey,\n });\n const publishSummary =\n typeof options.publishSummary === \"function\"\n ? await options.publishSummary(review)\n : options.publishSummary !== false;\n if (publishSummary) await updateReviewSummary(review, token);\n }\n\n async function reconcileReviewChecks(\n input: ReconcileReviewChecksInput,\n ): Promise<ReconcileReviewChecksResult> {\n const { tasks, ...reconcileOptions } = input;\n const reconciliationNow = reconcileOptions.now ?? options.now ?? (() => new Date());\n const batchSize = reconcileOptions.batchSize ?? reviewCheckBatchSize;\n if (!Number.isSafeInteger(batchSize) || batchSize <= 0) {\n throw new Error(\"Review-check batch size must be a positive safe integer\");\n }\n const candidates = tasks\n .filter(\n (task) => task.work.route?.id === \"code-review\" && terminalReviewStates.has(task.state),\n )\n .sort((left, right) => left.id.localeCompare(right.id));\n const candidatePage =\n reconcileOptions.candidatePage ??\n Math.floor(reconciliationNow().getTime() / reviewCheckPageIntervalMs);\n if (!Number.isSafeInteger(candidatePage) || candidatePage < 0) {\n throw new Error(\"Review-check candidate page must be a nonnegative safe integer\");\n }\n const pageCount = Math.max(1, Math.ceil(candidates.length / batchSize));\n const pageStart = (candidatePage % pageCount) * batchSize;\n const page = candidates.slice(pageStart, pageStart + batchSize);\n const pending = (\n await Promise.all(\n page.map(async (task) => ({\n task,\n effect: await options.stores.taskEffects.get({\n taskId: task.id,\n key: reviewCheckEffectKey,\n }),\n })),\n )\n ).filter(({ effect }) => effect?.resolvedAt === undefined);\n const concluded: TaskId[] = [];\n const stamped: TaskId[] = [];\n for (const { task, effect } of pending) {\n try {\n if (await concludeAbandonedCheck(task, effect, reconciliationNow)) concluded.push(task.id);\n await options.stores.taskEffects.resolve({\n taskId: task.id,\n key: reviewCheckEffectKey,\n at: reconciliationNow().toISOString(),\n });\n stamped.push(task.id);\n } catch {\n // A failed provider operation leaves the stamp absent so the next pass retries it.\n }\n }\n return { concluded, stamped };\n }\n\n async function recordReviewCheck(taskId: TaskId, checkRunId: number, headSha: string) {\n await options.stores.taskEffects.record({\n taskId,\n key: reviewCheckEffectKey,\n reference: {\n provider: \"github\",\n resourceId: String(checkRunId),\n subject: headSha,\n },\n });\n }\n\n async function updateReviewSummary(review: CompletedReview, token: string): Promise<void> {\n const comments = await listPullRequestComments({\n repository: review.repository,\n token,\n fetch: options.fetch,\n pullRequestNumber: review.pullRequestNumber,\n });\n const summary = findSummaryComment(comments);\n if (summary === undefined && review.comments.length === 0) return;\n\n const body = renderSummary({ active: review.comments.map(toFinding).sort(compareFindings) });\n const head = await readPullRequestHead({\n repository: review.repository,\n token,\n fetch: options.fetch,\n pullRequestNumber: review.pullRequestNumber,\n });\n if (head.sha !== review.headSha) return;\n\n if (summary === undefined) {\n await createPullRequestComment({\n repository: review.repository,\n token,\n fetch: options.fetch,\n pullRequestNumber: review.pullRequestNumber,\n body,\n });\n } else if (summary.body !== body) {\n await updatePullRequestComment({\n repository: review.repository,\n token,\n fetch: options.fetch,\n commentId: summary.id,\n body,\n });\n }\n }\n\n function renderSummary(state: ReviewSummaryState): string {\n const lines = [summaryMarker, `## ${summaryHeading}`, \"\"];\n if (state.active.length > 0) {\n lines.push(\n `${state.active.length} open`,\n \"\",\n \"### Open findings\",\n \"\",\n ...state.active.flatMap((finding) => [\n `- **${finding.title}** at \\`${finding.path}:${finding.line}\\``,\n ` ${finding.body.replaceAll(\"\\n\", \"\\n \")}`,\n ]),\n );\n } else {\n lines.push(\"✅ All findings resolved.\");\n }\n lines.push(\"\", encodeState(state));\n return lines.join(\"\\n\");\n }\n\n async function findReviewCheck(review: CompletedReview, token: string) {\n const checkRuns = await listCheckRunsForRef({\n repository: review.repository,\n token,\n fetch: options.fetch,\n ref: review.headSha,\n checkName,\n });\n const checkRun = checkRuns.find((candidate) => candidate.externalId === review.taskId);\n if (checkRun === undefined) {\n throw new Error(\n `could not find ${checkName} check for task ${review.taskId} on ${review.headSha}`,\n );\n }\n return checkRun;\n }\n\n async function concludeAbandonedCheck(\n task: Task,\n effect: TaskEffect | null,\n reconciliationNow: () => Date,\n ) {\n const change =\n task.changeId === undefined ? null : await options.stores.changes.get(task.changeId);\n if (change === null) return false;\n const repository = await options.stores.repositories.get(change.repositoryId);\n if (repository === null) return false;\n const token = await options.tokenForRepository(repository.slug);\n const checkRunId =\n recordedCheckRunId(effect) ??\n (await discoverCheckRun(repository.slug, token, task, change.prNumber));\n if (checkRunId === undefined) return false;\n const cancelled = task.state === \"cancelled\";\n await updateCheckRun({\n repository: repository.slug,\n token,\n fetch: options.fetch,\n checkRunId,\n status: \"completed\",\n conclusion: cancelled ? \"cancelled\" : \"neutral\",\n completedAt: reconciliationNow().toISOString(),\n output: {\n title: cancelled ? \"Review cancelled\" : \"Review did not complete\",\n summary: `The Factory code reviewer ended as ${task.state} without recording a verdict. Push a new commit to request a fresh review.`,\n },\n });\n return true;\n }\n\n async function discoverCheckRun(\n repository: RepositorySlug,\n token: string,\n task: Task,\n pullRequestNumber: number,\n ): Promise<number | undefined> {\n const head = await readPullRequestHead({\n repository,\n token,\n fetch: options.fetch,\n pullRequestNumber,\n });\n const checkRuns = await listCheckRunsForRef({\n repository,\n token,\n fetch: options.fetch,\n ref: head.sha,\n checkName,\n });\n return checkRuns.find(\n (candidate) => candidate.externalId === task.id && candidate.status !== \"completed\",\n )?.id;\n }\n\n return Object.freeze({ startReviewCheck, completeReviewCheck, reconcileReviewChecks });\n}\n\nfunction findSummaryComment(\n comments: readonly GitHubIssueComment[],\n): GitHubIssueComment | undefined {\n return comments.find(\n (comment) => comment.authorType === \"Bot\" && comment.body.includes(summaryMarker),\n );\n}\n\nfunction toFinding(finding: CompletedReviewFinding): ReviewFinding {\n return {\n fingerprint: finding.fingerprint,\n title: finding.title.replaceAll(/\\s+/g, \" \").trim().slice(0, 60),\n path: finding.path.replaceAll(\"`\", \"'\").slice(0, 120),\n line: finding.line,\n body: finding.body.trim(),\n };\n}\n\nfunction encodeState(state: ReviewSummaryState): string {\n return `${statePrefix}${Buffer.from(JSON.stringify(state)).toString(\"base64url\")}${stateSuffix}`;\n}\n\nfunction compareFindings(left: ReviewFinding, right: ReviewFinding): number {\n return left.path.localeCompare(right.path) || left.line - right.line;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"github-reporter.mjs","names":[],"sources":["../../src/code-review/github-reporter.ts"],"sourcesContent":["import {\n createCheckRun,\n createPullRequestComment,\n listCheckRunsForRef,\n listPullRequestComments,\n readPullRequestHead,\n updateCheckRun,\n updatePullRequestComment,\n type GitHubIssueComment,\n} from \"../github\";\nimport type { TaskId } from \"../schema/id\";\nimport type { Task } from \"../schema/task\";\nimport type { TaskEffect } from \"../schema/task-effect\";\nimport type { FactoryStores } from \"../store/engine\";\nimport type { DateClock } from \"../schema/common\";\nimport type { RepositorySlug } from \"../schema/repository\";\nimport type { CompletedReview, CompletedReviewFinding } from \"./contracts\";\nimport type { StartReviewCheckInput } from \"./preparation\";\n\n/** Storage, credentials, and presentation settings for GitHub review reporting. */\nexport interface GitHubReviewReporterOptions {\n stores: Pick<FactoryStores, \"tasks\" | \"taskEffects\" | \"changes\" | \"repositories\">;\n tokenForRepository(repositorySlug: RepositorySlug): Promise<string>;\n fetch?: typeof globalThis.fetch;\n checkName?: string;\n summaryHeading?: string;\n /** Whether to maintain a separate finding-summary comment. Defaults to true. */\n publishSummary?: boolean | ((review: CompletedReview) => boolean | Promise<boolean>);\n now?: DateClock;\n}\n\n/** Check-run and pull-request summary lifecycle for supplied code reviews. */\nexport interface GitHubReviewReporter {\n startReviewCheck(input: StartReviewCheckInput): Promise<void>;\n completeReviewCheck(review: CompletedReview): Promise<void>;\n /** Conclude the current attempt's obsolete check before a replacement is admitted. */\n supersedeReviewCheck(task: Task): Promise<void>;\n /** Read the commit pinned by the current attempt's recorded check. */\n readReviewHead(task: Task): Promise<string | null>;\n reconcileReviewChecks(input: ReconcileReviewChecksInput): Promise<ReconcileReviewChecksResult>;\n}\n\n/** Bounded rotating-page controls for abandoned review-check recovery. */\nexport interface ReconcileReviewChecksOptions {\n batchSize?: number;\n /** Stable page selector; defaults to the current five-minute recovery window. */\n candidatePage?: number;\n now?: DateClock;\n}\n\n/** Review Tasks and bounded recovery controls for one reconciliation pass. */\nexport interface ReconcileReviewChecksInput extends ReconcileReviewChecksOptions {\n tasks: readonly Task[];\n}\n\n/** Review Tasks whose check effects were concluded or durably resolved in one pass. */\nexport interface ReconcileReviewChecksResult {\n concluded: readonly TaskId[];\n stamped: readonly TaskId[];\n}\n\ninterface ReviewFinding {\n fingerprint: string;\n title: string;\n path: string;\n line: number;\n body: string;\n}\n\ninterface ReviewSummaryState {\n active: ReviewFinding[];\n}\n\nconst summaryMarker = \"<!-- factory-code-review-summary -->\";\nconst statePrefix = \"<!-- factory-code-review-state:v1:\";\nconst stateSuffix = \" -->\";\nconst terminalReviewStates: ReadonlySet<Task[\"state\"]> = new Set([\"cancelled\", \"failed\"]);\nconst reviewCheckBatchSize = 20;\nconst reviewCheckPageIntervalMs = 300_000;\nfunction reviewCheckEffectKey(attempt: number) {\n if (!Number.isSafeInteger(attempt) || attempt < 1)\n throw new Error(\"Review checks require a positive attempt\");\n return {\n namespace: \"software\",\n name: attempt === 1 ? \"github_review_check\" : `github_review_check_${attempt}`,\n } as const;\n}\n\nfunction reviewCheckExternalId(taskId: TaskId, attempt: number) {\n return attempt === 1 ? taskId : `${taskId}:${attempt}`;\n}\n\nfunction recordedCheckRunId(effect: TaskEffect | null, headSha?: string): number | undefined {\n if (effect?.reference === undefined) return undefined;\n const { provider, resourceId } = effect.reference;\n const checkRunId = Number(resourceId);\n if (provider !== \"github\" || !Number.isSafeInteger(checkRunId) || checkRunId <= 0) {\n throw new Error(\n \"Stored software review-check effect has an invalid GitHub check-run reference\",\n );\n }\n if (headSha !== undefined && effect.reference.subject !== headSha) {\n throw new Error(\n `Review task ${effect.taskId} has a check for another commit. Request a new review for ${headSha}.`,\n );\n }\n return checkRunId;\n}\n\n/** Create the supplied GitHub check-run and pull-request summary reporter for code reviews. */\nexport function createGitHubReviewReporter(\n options: GitHubReviewReporterOptions,\n): GitHubReviewReporter {\n const checkName = options.checkName ?? \"Factory code review\";\n const summaryHeading = options.summaryHeading ?? \"Factory code review\";\n const now = () => (options.now ?? (() => new Date()))().toISOString();\n\n async function startReviewCheck(input: StartReviewCheckInput): Promise<void> {\n const key = reviewCheckEffectKey(input.attempt);\n if (input.attempt > 1) {\n const task = await options.stores.tasks.get(input.taskId);\n if (!task) throw new Error(\"Review Task is unavailable\");\n for (const attempt of new Set([1, input.attempt - 1]))\n await supersedeReviewCheck({ ...task, attempt });\n }\n const token = await options.tokenForRepository(input.repository);\n if (input.reuseExisting) {\n const effect = await options.stores.taskEffects.get({\n taskId: input.taskId,\n key,\n });\n if (\n recordedCheckRunId(effect, input.headSha) !== undefined ||\n effect?.resolvedAt !== undefined\n )\n return;\n const checks = await listCheckRunsForRef({\n repository: input.repository,\n token,\n fetch: options.fetch,\n ref: input.headSha,\n checkName,\n });\n const existing = checks.find(\n (check) => check.externalId === reviewCheckExternalId(input.taskId, input.attempt),\n );\n if (existing !== undefined) {\n await recordReviewCheck(input.taskId, input.attempt, existing.id, input.headSha);\n return;\n }\n }\n const checkRun = await createCheckRun({\n repository: input.repository,\n token,\n fetch: options.fetch,\n name: checkName,\n headSha: input.headSha,\n status: \"in_progress\",\n externalId: reviewCheckExternalId(input.taskId, input.attempt),\n detailsUrl: input.pullRequestUrl,\n startedAt: now(),\n output: {\n title: \"Review in progress\",\n summary: \"The Factory code reviewer is inspecting this commit.\",\n },\n });\n await recordReviewCheck(input.taskId, input.attempt, checkRun.id, input.headSha);\n }\n\n async function completeReviewCheck(review: CompletedReview): Promise<void> {\n const token = await options.tokenForRepository(review.repository);\n const effect = await options.stores.taskEffects.get({\n taskId: review.taskId,\n key: reviewCheckEffectKey(review.attempt),\n });\n let checkRunId = recordedCheckRunId(effect, review.headSha);\n if (checkRunId === undefined) {\n checkRunId = (await findReviewCheck(review, token)).id;\n await recordReviewCheck(review.taskId, review.attempt, checkRunId, review.headSha);\n }\n await updateCheckRun({\n repository: review.repository,\n token,\n fetch: options.fetch,\n checkRunId,\n status: \"completed\",\n conclusion: review.verdict === \"request_changes\" ? \"failure\" : \"success\",\n completedAt: now(),\n output: {\n title:\n review.verdict === \"request_changes\" ? \"Review found blocking issues\" : \"Review complete\",\n summary: review.summary,\n annotations: review.comments.map((comment) => ({\n path: comment.path,\n startLine: comment.startLine ?? comment.line,\n endLine: comment.line,\n annotationLevel: review.verdict === \"request_changes\" ? \"failure\" : \"notice\",\n title: comment.title,\n message: comment.body,\n })),\n },\n });\n await options.stores.taskEffects.resolve({\n taskId: review.taskId,\n key: reviewCheckEffectKey(review.attempt),\n });\n const publishSummary =\n typeof options.publishSummary === \"function\"\n ? await options.publishSummary(review)\n : options.publishSummary !== false;\n if (publishSummary) await updateReviewSummary(review, token);\n }\n\n async function reconcileReviewChecks(\n input: ReconcileReviewChecksInput,\n ): Promise<ReconcileReviewChecksResult> {\n const { tasks, ...reconcileOptions } = input;\n const reconciliationNow = reconcileOptions.now ?? options.now ?? (() => new Date());\n const batchSize = reconcileOptions.batchSize ?? reviewCheckBatchSize;\n if (!Number.isSafeInteger(batchSize) || batchSize <= 0) {\n throw new Error(\"Review-check batch size must be a positive safe integer\");\n }\n const candidates = tasks\n .filter(\n (task) => task.work.route?.id === \"code-review\" && terminalReviewStates.has(task.state),\n )\n .sort((left, right) => left.id.localeCompare(right.id));\n const candidatePage =\n reconcileOptions.candidatePage ??\n Math.floor(reconciliationNow().getTime() / reviewCheckPageIntervalMs);\n if (!Number.isSafeInteger(candidatePage) || candidatePage < 0) {\n throw new Error(\"Review-check candidate page must be a nonnegative safe integer\");\n }\n const pageCount = Math.max(1, Math.ceil(candidates.length / batchSize));\n const pageStart = (candidatePage % pageCount) * batchSize;\n const page = candidates.slice(pageStart, pageStart + batchSize);\n const attemptPage = Math.floor(candidatePage / pageCount);\n const pending = (\n await Promise.all(\n page.map(async (task) => {\n const attempts = new Set([task.attempt, 1]);\n if (task.attempt > 2) attempts.add(task.attempt - 1 - (attemptPage % (task.attempt - 2)));\n const effects = await Promise.all(\n [...attempts].map(async (attempt) => {\n const key = reviewCheckEffectKey(attempt);\n const effect = await options.stores.taskEffects.get({ taskId: task.id, key });\n return { task, attempt, key, effect };\n }),\n );\n const recorded = effects.filter(\n ({ effect }) => effect?.reference !== undefined && effect.resolvedAt === undefined,\n );\n // Recorded checks include obsolete heads and older deployments' original key.\n if (recorded.length > 0) return recorded;\n return effects.filter(\n ({ attempt, effect }) => attempt === task.attempt && effect?.resolvedAt === undefined,\n );\n }),\n )\n ).flat();\n const concluded = new Set<TaskId>();\n const stamped = new Set<TaskId>();\n for (const { task, key, effect } of pending) {\n try {\n if (await concludeAbandonedCheck(task, effect, reconciliationNow)) concluded.add(task.id);\n await options.stores.taskEffects.resolve({\n taskId: task.id,\n key,\n at: reconciliationNow().toISOString(),\n });\n stamped.add(task.id);\n } catch {\n // A failed provider operation leaves the stamp absent so the next pass retries it.\n }\n }\n return { concluded: [...concluded], stamped: [...stamped] };\n }\n\n async function recordReviewCheck(\n taskId: TaskId,\n attempt: number,\n checkRunId: number,\n headSha: string,\n ) {\n await options.stores.taskEffects.record({\n taskId,\n key: reviewCheckEffectKey(attempt),\n reference: {\n provider: \"github\",\n resourceId: String(checkRunId),\n subject: headSha,\n },\n });\n }\n\n async function supersedeReviewCheck(task: Task): Promise<void> {\n const key = reviewCheckEffectKey(task.attempt);\n const effect = await options.stores.taskEffects.get({ taskId: task.id, key });\n if (!effect?.reference || effect.resolvedAt) return;\n if (!(await concludeAbandonedCheck(task, effect, options.now ?? (() => new Date()), true)))\n throw new Error(\"Obsolete review check could not be closed\");\n await options.stores.taskEffects.resolve({ taskId: task.id, key });\n }\n\n async function readReviewHead(task: Task): Promise<string | null> {\n const effect = await options.stores.taskEffects.get({\n taskId: task.id,\n key: reviewCheckEffectKey(task.attempt),\n });\n recordedCheckRunId(effect);\n return effect?.reference?.subject ?? null;\n }\n\n async function updateReviewSummary(review: CompletedReview, token: string): Promise<void> {\n const comments = await listPullRequestComments({\n repository: review.repository,\n token,\n fetch: options.fetch,\n pullRequestNumber: review.pullRequestNumber,\n });\n const summary = findSummaryComment(comments);\n if (summary === undefined && review.comments.length === 0) return;\n\n const body = renderSummary({ active: review.comments.map(toFinding).sort(compareFindings) });\n const head = await readPullRequestHead({\n repository: review.repository,\n token,\n fetch: options.fetch,\n pullRequestNumber: review.pullRequestNumber,\n });\n if (head.sha !== review.headSha) return;\n\n if (summary === undefined) {\n await createPullRequestComment({\n repository: review.repository,\n token,\n fetch: options.fetch,\n pullRequestNumber: review.pullRequestNumber,\n body,\n });\n } else if (summary.body !== body) {\n await updatePullRequestComment({\n repository: review.repository,\n token,\n fetch: options.fetch,\n commentId: summary.id,\n body,\n });\n }\n }\n\n function renderSummary(state: ReviewSummaryState): string {\n const lines = [summaryMarker, `## ${summaryHeading}`, \"\"];\n if (state.active.length > 0) {\n lines.push(\n `${state.active.length} open`,\n \"\",\n \"### Open findings\",\n \"\",\n ...state.active.flatMap((finding) => [\n `- **${finding.title}** at \\`${finding.path}:${finding.line}\\``,\n ` ${finding.body.replaceAll(\"\\n\", \"\\n \")}`,\n ]),\n );\n } else {\n lines.push(\"✅ All findings resolved.\");\n }\n lines.push(\"\", encodeState(state));\n return lines.join(\"\\n\");\n }\n\n async function findReviewCheck(review: CompletedReview, token: string) {\n const checkRuns = await listCheckRunsForRef({\n repository: review.repository,\n token,\n fetch: options.fetch,\n ref: review.headSha,\n checkName,\n });\n const checkRun = checkRuns.find(\n (candidate) => candidate.externalId === reviewCheckExternalId(review.taskId, review.attempt),\n );\n if (checkRun === undefined) {\n throw new Error(\n `could not find ${checkName} check for task ${review.taskId} on ${review.headSha}`,\n );\n }\n return checkRun;\n }\n\n async function concludeAbandonedCheck(\n task: Task,\n effect: TaskEffect | null,\n reconciliationNow: () => Date,\n cancelled = task.state === \"cancelled\",\n ) {\n const change =\n task.changeId === undefined ? null : await options.stores.changes.get(task.changeId);\n if (change === null) return false;\n const repository = await options.stores.repositories.get(change.repositoryId);\n if (repository === null) return false;\n const token = await options.tokenForRepository(repository.slug);\n const checkRunId =\n recordedCheckRunId(effect) ??\n (await discoverCheckRun(repository.slug, token, task, change.prNumber));\n if (checkRunId === undefined) return false;\n await updateCheckRun({\n repository: repository.slug,\n token,\n fetch: options.fetch,\n checkRunId,\n status: \"completed\",\n conclusion: cancelled ? \"cancelled\" : \"neutral\",\n completedAt: reconciliationNow().toISOString(),\n output: {\n title: cancelled ? \"Review cancelled\" : \"Review did not complete\",\n summary: `This review attempt ended as ${task.state} without recording a verdict.`,\n },\n });\n return true;\n }\n\n async function discoverCheckRun(\n repository: RepositorySlug,\n token: string,\n task: Task,\n pullRequestNumber: number,\n ): Promise<number | undefined> {\n const head = await readPullRequestHead({\n repository,\n token,\n fetch: options.fetch,\n pullRequestNumber,\n });\n const checkRuns = await listCheckRunsForRef({\n repository,\n token,\n fetch: options.fetch,\n ref: head.sha,\n checkName,\n });\n return checkRuns.find(\n (candidate) =>\n candidate.externalId === reviewCheckExternalId(task.id, task.attempt) &&\n candidate.status !== \"completed\",\n )?.id;\n }\n\n return Object.freeze({\n startReviewCheck,\n completeReviewCheck,\n supersedeReviewCheck,\n readReviewHead,\n reconcileReviewChecks,\n });\n}\n\nfunction findSummaryComment(\n comments: readonly GitHubIssueComment[],\n): GitHubIssueComment | undefined {\n return comments.find(\n (comment) => comment.authorType === \"Bot\" && comment.body.includes(summaryMarker),\n );\n}\n\nfunction toFinding(finding: CompletedReviewFinding): ReviewFinding {\n return {\n fingerprint: finding.fingerprint,\n title: finding.title.replaceAll(/\\s+/g, \" \").trim().slice(0, 60),\n path: finding.path.replaceAll(\"`\", \"'\").slice(0, 120),\n line: finding.line,\n body: finding.body.trim(),\n };\n}\n\nfunction encodeState(state: ReviewSummaryState): string {\n return `${statePrefix}${Buffer.from(JSON.stringify(state)).toString(\"base64url\")}${stateSuffix}`;\n}\n\nfunction compareFindings(left: ReviewFinding, right: ReviewFinding): number {\n return left.path.localeCompare(right.path) || left.line - right.line;\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TaskId } from "../schema/id.mjs";
|
|
2
|
-
import { RepositorySlug } from "../schema/repository.mjs";
|
|
3
2
|
import { Task } from "../schema/task.mjs";
|
|
3
|
+
import { RepositorySlug } from "../schema/repository.mjs";
|
|
4
4
|
import { FactoryStores } from "../store/engine.mjs";
|
|
5
5
|
//#region src/code-review/preparation.d.ts
|
|
6
6
|
/** Commit-fenced pull-request details used to begin a review check run. */
|
|
@@ -8,6 +8,7 @@ interface StartReviewCheckInput {
|
|
|
8
8
|
repository: RepositorySlug;
|
|
9
9
|
headSha: string;
|
|
10
10
|
taskId: TaskId;
|
|
11
|
+
attempt: number;
|
|
11
12
|
pullRequestUrl: string;
|
|
12
13
|
reuseExisting?: boolean;
|
|
13
14
|
}
|
|
@@ -13,9 +13,10 @@ async function preparePullRequestReview(input) {
|
|
|
13
13
|
repository: repository.slug,
|
|
14
14
|
headSha,
|
|
15
15
|
taskId: input.task.id,
|
|
16
|
+
attempt: input.task.attempt,
|
|
16
17
|
pullRequestUrl: change.prUrl
|
|
17
18
|
});
|
|
18
|
-
return `${input.message}\n\nPull request: ${change.prUrl}.\nExpected head commit: ${headSha}.\nReview only this head and finish with record_post_review.`;
|
|
19
|
+
return `${input.message.replace(/^Expected head commit:.*$/gmu, "").trim()}\n\nPull request: ${change.prUrl}.\nExpected head commit: ${headSha}.\nReview only this head and finish with record_post_review.`;
|
|
19
20
|
}
|
|
20
21
|
//#endregion
|
|
21
22
|
export { preparePullRequestReview };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preparation.mjs","names":[],"sources":["../../src/code-review/preparation.ts"],"sourcesContent":["import type { TaskId } from \"../schema/id\";\nimport type { RepositorySlug } from \"../schema/repository\";\nimport type { Task } from \"../schema/task\";\nimport type { FactoryStores } from \"../store/engine\";\n\n/** Commit-fenced pull-request details used to begin a review check run. */\nexport interface StartReviewCheckInput {\n repository: RepositorySlug;\n headSha: string;\n taskId: TaskId;\n pullRequestUrl: string;\n reuseExisting?: boolean;\n}\n\n/** Provider operations used to fence and initialize a pull-request review. */\nexport interface PreparePullRequestReviewOptions {\n readHead(input: ReadPullRequestReviewHeadInput): Promise<string>;\n startCheck(input: Omit<StartReviewCheckInput, \"reuseExisting\">): Promise<void>;\n}\n\n/** Repository and pull-request identity used to read a review target head. */\nexport interface ReadPullRequestReviewHeadInput {\n repository: RepositorySlug;\n pullRequestNumber: number;\n}\n\n/** Stores, Task, message, and provider operations used to prepare one review. */\nexport interface PreparePullRequestReviewInput extends PreparePullRequestReviewOptions {\n stores: Pick<FactoryStores, \"changes\" | \"repositories\">;\n task: Task;\n message: string;\n}\n\n/** Prepare the linked pull request head and reporting check before launching a reviewer. */\nexport async function preparePullRequestReview(\n input: PreparePullRequestReviewInput,\n): Promise<string> {\n const change =\n input.task.changeId === undefined ? null : await input.stores.changes.get(input.task.changeId);\n if (\n change === null ||\n input.task.repositoryIds.length !== 1 ||\n change.repositoryId !== input.task.repositoryIds[0]\n ) {\n throw new Error(\"Code review requires its linked Change\");\n }\n const repository = await input.stores.repositories.get(change.repositoryId);\n if (repository === null || !repository.enabled) {\n throw new Error(\"Code review repository is unavailable\");\n }\n const headSha = await input.readHead({\n repository: repository.slug,\n pullRequestNumber: change.prNumber,\n });\n await input.startCheck({\n repository: repository.slug,\n headSha,\n taskId: input.task.id,\n pullRequestUrl: change.prUrl,\n });\n return `${input.message}\\n\\nPull request: ${change.prUrl}.\\nExpected head commit: ${headSha}.\\nReview only this head and finish with record_post_review.`;\n}\n"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"preparation.mjs","names":[],"sources":["../../src/code-review/preparation.ts"],"sourcesContent":["import type { TaskId } from \"../schema/id\";\nimport type { RepositorySlug } from \"../schema/repository\";\nimport type { Task } from \"../schema/task\";\nimport type { FactoryStores } from \"../store/engine\";\n\n/** Commit-fenced pull-request details used to begin a review check run. */\nexport interface StartReviewCheckInput {\n repository: RepositorySlug;\n headSha: string;\n taskId: TaskId;\n attempt: number;\n pullRequestUrl: string;\n reuseExisting?: boolean;\n}\n\n/** Provider operations used to fence and initialize a pull-request review. */\nexport interface PreparePullRequestReviewOptions {\n readHead(input: ReadPullRequestReviewHeadInput): Promise<string>;\n startCheck(input: Omit<StartReviewCheckInput, \"reuseExisting\">): Promise<void>;\n}\n\n/** Repository and pull-request identity used to read a review target head. */\nexport interface ReadPullRequestReviewHeadInput {\n repository: RepositorySlug;\n pullRequestNumber: number;\n}\n\n/** Stores, Task, message, and provider operations used to prepare one review. */\nexport interface PreparePullRequestReviewInput extends PreparePullRequestReviewOptions {\n stores: Pick<FactoryStores, \"changes\" | \"repositories\">;\n task: Task;\n message: string;\n}\n\n/** Prepare the linked pull request head and reporting check before launching a reviewer. */\nexport async function preparePullRequestReview(\n input: PreparePullRequestReviewInput,\n): Promise<string> {\n const change =\n input.task.changeId === undefined ? null : await input.stores.changes.get(input.task.changeId);\n if (\n change === null ||\n input.task.repositoryIds.length !== 1 ||\n change.repositoryId !== input.task.repositoryIds[0]\n ) {\n throw new Error(\"Code review requires its linked Change\");\n }\n const repository = await input.stores.repositories.get(change.repositoryId);\n if (repository === null || !repository.enabled) {\n throw new Error(\"Code review repository is unavailable\");\n }\n const headSha = await input.readHead({\n repository: repository.slug,\n pullRequestNumber: change.prNumber,\n });\n await input.startCheck({\n repository: repository.slug,\n headSha,\n taskId: input.task.id,\n attempt: input.task.attempt,\n pullRequestUrl: change.prUrl,\n });\n return `${input.message.replace(/^Expected head commit:.*$/gmu, \"\").trim()}\\n\\nPull request: ${change.prUrl}.\\nExpected head commit: ${headSha}.\\nReview only this head and finish with record_post_review.`;\n}\n"],"mappings":";;AAmCA,eAAsB,yBACpB,OACiB;CACjB,MAAM,SACJ,MAAM,KAAK,aAAa,KAAA,IAAY,OAAO,MAAM,MAAM,OAAO,QAAQ,IAAI,MAAM,KAAK,QAAQ;CAC/F,IACE,WAAW,QACX,MAAM,KAAK,cAAc,WAAW,KACpC,OAAO,iBAAiB,MAAM,KAAK,cAAc,IAEjD,MAAM,IAAI,MAAM,wCAAwC;CAE1D,MAAM,aAAa,MAAM,MAAM,OAAO,aAAa,IAAI,OAAO,YAAY;CAC1E,IAAI,eAAe,QAAQ,CAAC,WAAW,SACrC,MAAM,IAAI,MAAM,uCAAuC;CAEzD,MAAM,UAAU,MAAM,MAAM,SAAS;EACnC,YAAY,WAAW;EACvB,mBAAmB,OAAO;CAC5B,CAAC;CACD,MAAM,MAAM,WAAW;EACrB,YAAY,WAAW;EACvB;EACA,QAAQ,MAAM,KAAK;EACnB,SAAS,MAAM,KAAK;EACpB,gBAAgB,OAAO;CACzB,CAAC;CACD,OAAO,GAAG,MAAM,QAAQ,QAAQ,gCAAgC,EAAE,CAAC,CAAC,KAAK,EAAE,oBAAoB,OAAO,MAAM,2BAA2B,QAAQ;AACjJ"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFactoryRecords } from "../store/iterate.mjs";
|
|
1
2
|
import { sweepTaskStalls } from "../stall.mjs";
|
|
2
3
|
//#region src/code-review/stall.ts
|
|
3
4
|
const supersedingReviewStates = /* @__PURE__ */ new Set([
|
|
@@ -7,6 +8,7 @@ const supersedingReviewStates = /* @__PURE__ */ new Set([
|
|
|
7
8
|
"needs_human"
|
|
8
9
|
]);
|
|
9
10
|
function supersedingCodeReview(task, tasks) {
|
|
11
|
+
if (task.origin.operator !== void 0) return void 0;
|
|
10
12
|
if (task.work.route?.id !== "code-review" || task.changeId === void 0) return void 0;
|
|
11
13
|
return tasks.find((candidate) => candidate.id !== task.id && candidate.work.route?.id === "code-review" && !candidate.waitingForMessages && candidate.changeId === task.changeId && supersedingReviewStates.has(candidate.state) && Date.parse(candidate.createdAt) > Date.parse(task.createdAt));
|
|
12
14
|
}
|
|
@@ -14,8 +16,15 @@ function supersedingCodeReview(task, tasks) {
|
|
|
14
16
|
async function sweepCodeReviewStalls(stores, options) {
|
|
15
17
|
const { cancelled: superseded, ...generic } = await sweepTaskStalls(stores, {
|
|
16
18
|
...options,
|
|
17
|
-
cancelStalled(task, tasks) {
|
|
18
|
-
const successor = supersedingCodeReview(task, tasks
|
|
19
|
+
async cancelStalled(task, tasks) {
|
|
20
|
+
const successor = supersedingCodeReview(task, options.tasks || !task.changeId || task.work.route?.id !== "code-review" ? tasks : await collectFactoryRecords(stores.tasks.list, {
|
|
21
|
+
limit: 100,
|
|
22
|
+
match: [...supersedingReviewStates].map((state) => ({
|
|
23
|
+
state,
|
|
24
|
+
changeId: task.changeId,
|
|
25
|
+
work: { route: { id: "code-review" } }
|
|
26
|
+
}))
|
|
27
|
+
}));
|
|
19
28
|
return successor === void 0 ? null : `review superseded by newer commits: task ${successor.id} reviews the current head`;
|
|
20
29
|
}
|
|
21
30
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stall.mjs","names":[],"sources":["../../src/code-review/stall.ts"],"sourcesContent":["import type { TaskId } from \"../schema/id\";\nimport type { Task } from \"../schema/task\";\nimport type { FactoryStores } from \"../store/engine\";\nimport { sweepTaskStalls, type StallSweepOptions, type TaskStallSweepResult } from \"../stall\";\n\n/** Task capability required by code-review stall supersession. */\nexport type CodeReviewStallSweepStores = Pick<FactoryStores, \"tasks\">;\n\nconst supersedingReviewStates: ReadonlySet<Task[\"state\"]> = new Set([\n \"queued\",\n \"running\",\n \"stalled\",\n \"needs_human\",\n]);\n\nfunction supersedingCodeReview(task: Task, tasks: readonly Task[]): Task | undefined {\n if (task.work.route?.id !== \"code-review\" || task.changeId === undefined) return undefined;\n return tasks.find(\n (candidate) =>\n candidate.id !== task.id &&\n candidate.work.route?.id === \"code-review\" &&\n !candidate.waitingForMessages &&\n candidate.changeId === task.changeId &&\n supersedingReviewStates.has(candidate.state) &&\n Date.parse(candidate.createdAt) > Date.parse(task.createdAt),\n );\n}\n\n/** Generic stall-sweep controls with the supplied review supersession policy fixed. */\nexport type SweepCodeReviewStallsOptions = Omit<StallSweepOptions, \"cancelStalled\">;\n\n/** Generic stall results with obsolete code reviews reported as superseded. */\nexport interface SweepCodeReviewStallsResult extends Omit<TaskStallSweepResult, \"cancelled\"> {\n readonly superseded: readonly TaskId[];\n}\n\n/** Apply the supplied code-review supersession policy around the generic stall sweep. */\nexport async function sweepCodeReviewStalls(\n stores: CodeReviewStallSweepStores,\n options: SweepCodeReviewStallsOptions,\n): Promise<SweepCodeReviewStallsResult> {\n const result = await sweepTaskStalls(stores, {\n ...options,\n cancelStalled(task, tasks) {\n const successor = supersedingCodeReview(task,
|
|
1
|
+
{"version":3,"file":"stall.mjs","names":[],"sources":["../../src/code-review/stall.ts"],"sourcesContent":["import { collectFactoryRecords } from \"../store/iterate\";\nimport type { TaskId } from \"../schema/id\";\nimport type { Task } from \"../schema/task\";\nimport type { FactoryStores } from \"../store/engine\";\nimport { sweepTaskStalls, type StallSweepOptions, type TaskStallSweepResult } from \"../stall\";\n\n/** Task capability required by code-review stall supersession. */\nexport type CodeReviewStallSweepStores = Pick<FactoryStores, \"tasks\">;\n\nconst supersedingReviewStates: ReadonlySet<Task[\"state\"]> = new Set([\n \"queued\",\n \"running\",\n \"stalled\",\n \"needs_human\",\n]);\n\nfunction supersedingCodeReview(task: Task, tasks: readonly Task[]): Task | undefined {\n if (task.origin.operator !== undefined) return undefined;\n if (task.work.route?.id !== \"code-review\" || task.changeId === undefined) return undefined;\n return tasks.find(\n (candidate) =>\n candidate.id !== task.id &&\n candidate.work.route?.id === \"code-review\" &&\n !candidate.waitingForMessages &&\n candidate.changeId === task.changeId &&\n supersedingReviewStates.has(candidate.state) &&\n Date.parse(candidate.createdAt) > Date.parse(task.createdAt),\n );\n}\n\n/** Generic stall-sweep controls with the supplied review supersession policy fixed. */\nexport type SweepCodeReviewStallsOptions = Omit<StallSweepOptions, \"cancelStalled\">;\n\n/** Generic stall results with obsolete code reviews reported as superseded. */\nexport interface SweepCodeReviewStallsResult extends Omit<TaskStallSweepResult, \"cancelled\"> {\n readonly superseded: readonly TaskId[];\n}\n\n/** Apply the supplied code-review supersession policy around the generic stall sweep. */\nexport async function sweepCodeReviewStalls(\n stores: CodeReviewStallSweepStores,\n options: SweepCodeReviewStallsOptions,\n): Promise<SweepCodeReviewStallsResult> {\n const result = await sweepTaskStalls(stores, {\n ...options,\n async cancelStalled(task, tasks) {\n const candidates =\n options.tasks || !task.changeId || task.work.route?.id !== \"code-review\"\n ? tasks\n : await collectFactoryRecords(stores.tasks.list, {\n limit: 100,\n match: [...supersedingReviewStates].map((state) => ({\n state,\n changeId: task.changeId,\n work: { route: { id: \"code-review\" } },\n })),\n });\n const successor = supersedingCodeReview(task, candidates);\n return successor === undefined\n ? null\n : `review superseded by newer commits: task ${successor.id} reviews the current head`;\n },\n });\n const { cancelled: superseded, ...generic } = result;\n return { ...generic, superseded };\n}\n"],"mappings":";;;AASA,MAAM,0CAAsD,IAAI,IAAI;CAClE;CACA;CACA;CACA;AACF,CAAC;AAED,SAAS,sBAAsB,MAAY,OAA0C;CACnF,IAAI,KAAK,OAAO,aAAa,KAAA,GAAW,OAAO,KAAA;CAC/C,IAAI,KAAK,KAAK,OAAO,OAAO,iBAAiB,KAAK,aAAa,KAAA,GAAW,OAAO,KAAA;CACjF,OAAO,MAAM,MACV,cACC,UAAU,OAAO,KAAK,MACtB,UAAU,KAAK,OAAO,OAAO,iBAC7B,CAAC,UAAU,sBACX,UAAU,aAAa,KAAK,YAC5B,wBAAwB,IAAI,UAAU,KAAK,KAC3C,KAAK,MAAM,UAAU,SAAS,IAAI,KAAK,MAAM,KAAK,SAAS,CAC/D;AACF;;AAWA,eAAsB,sBACpB,QACA,SACsC;CAqBtC,MAAM,EAAE,WAAW,YAAY,GAAG,YAAY,MApBzB,gBAAgB,QAAQ;EAC3C,GAAG;EACH,MAAM,cAAc,MAAM,OAAO;GAY/B,MAAM,YAAY,sBAAsB,MAVtC,QAAQ,SAAS,CAAC,KAAK,YAAY,KAAK,KAAK,OAAO,OAAO,gBACvD,QACA,MAAM,sBAAsB,OAAO,MAAM,MAAM;IAC7C,OAAO;IACP,OAAO,CAAC,GAAG,uBAAuB,CAAC,CAAC,KAAK,WAAW;KAClD;KACA,UAAU,KAAK;KACf,MAAM,EAAE,OAAO,EAAE,IAAI,cAAc,EAAE;IACvC,EAAE;GACJ,CAAC,CACiD;GACxD,OAAO,cAAc,KAAA,IACjB,OACA,4CAA4C,UAAU,GAAG;EAC/D;CACF,CAAC;CAED,OAAO;EAAE,GAAG;EAAS;CAAW;AAClC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { RepositorySlug } from "../schema/repository.mjs";
|
|
2
1
|
import { MaybePromise } from "../schema/common.mjs";
|
|
3
2
|
import { CommunicationEvent } from "../schema/communication.mjs";
|
|
3
|
+
import { RepositorySlug } from "../schema/repository.mjs";
|
|
4
4
|
import { CommunicationAdapter } from "./index.mjs";
|
|
5
5
|
//#region src/connectors/github.d.ts
|
|
6
6
|
/** A GitHub repository and issue or pull-request number encoded by a reply destination. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { SignalSource } from "../schema/signal.mjs";
|
|
2
1
|
import { ReplyTo } from "../schema/common.mjs";
|
|
3
2
|
import { CommunicationEvent, CommunicationOutcome, communicationEventSchema, communicationOutcomeSchema, communicationOutcomes } from "../schema/communication.mjs";
|
|
3
|
+
import { SignalSource } from "../schema/signal.mjs";
|
|
4
4
|
import { CommunicationOutboxStores, CreateOutboxCommunicationRouterOptions, OutboxDeliveryOptions, OutboxSweepResult, SweepCommunicationOutboxOptions, createOutboxCommunicationRouter, sweepCommunicationOutbox } from "./outbox.mjs";
|
|
5
5
|
//#region src/connectors/index.d.ts
|
|
6
6
|
/** An authenticated provider webhook envelope before connector-specific normalization. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { RepositorySlug } from "../schema/repository.mjs";
|
|
2
1
|
import { MaybePromise } from "../schema/common.mjs";
|
|
3
2
|
import { CommunicationEvent } from "../schema/communication.mjs";
|
|
3
|
+
import { RepositorySlug } from "../schema/repository.mjs";
|
|
4
4
|
import { CommunicationAdapter, NormalizedSignal } from "./index.mjs";
|
|
5
5
|
import { SlackApprovals, SlackApprovalsOptions, SlackApprovalsStores, createSlackApprovals } from "./slack-approvals.mjs";
|
|
6
6
|
//#region src/connectors/slack.d.ts
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Signal } from "../schema/signal.mjs";
|
|
2
1
|
import { Task } from "../schema/task.mjs";
|
|
3
2
|
import { TaskGraph } from "../schema/task-graph.mjs";
|
|
4
3
|
import { Change, ChangeLevel } from "../schema/change.mjs";
|
|
5
4
|
import { Receipt } from "../schema/receipt.mjs";
|
|
5
|
+
import { Signal } from "../schema/signal.mjs";
|
|
6
6
|
//#region src/delivery-metrics/metrics.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* The derived review bucket from the default risk x confidence table. Derived
|
package/dist/dispatch.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { budgetMonthOf, canReserve, computeMonthSpendUsd } from "./budget.mjs";
|
|
1
|
+
import { budgetMonthOf, budgetTasks, canReserve, computeMonthSpendUsd } from "./budget.mjs";
|
|
2
|
+
import { RecordNotFoundError, VersionConflictError } from "./store/driver.mjs";
|
|
2
3
|
import { InvalidTransitionError } from "./schema/transitions.mjs";
|
|
3
4
|
import { routeKey } from "./agent-routes.mjs";
|
|
4
|
-
import { RecordNotFoundError, VersionConflictError } from "./store/driver.mjs";
|
|
5
5
|
//#region src/dispatch.ts
|
|
6
6
|
/**
|
|
7
7
|
* The provider may have accepted the request, so the running attempt must remain available for
|
|
@@ -121,7 +121,7 @@ async function dispatchTask(stores, input) {
|
|
|
121
121
|
if (options.budget !== void 0 && task.state === "queued") {
|
|
122
122
|
const budgetNow = (options.budget.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
123
123
|
const month = options.budget.month ?? budgetMonthOf(budgetNow);
|
|
124
|
-
const rows = (options.budget.enforce === false ? [] : options.budget.tasks ?? await stores
|
|
124
|
+
const rows = (options.budget.enforce === false ? [] : options.budget.tasks ?? await budgetTasks(stores)).map((row) => row.id === taskId ? {
|
|
125
125
|
...row,
|
|
126
126
|
reservedUsd: void 0
|
|
127
127
|
} : row);
|
package/dist/dispatch.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.mjs","names":[],"sources":["../src/dispatch.ts"],"sourcesContent":["import { budgetMonthOf, canReserve, computeMonthSpendUsd } from \"./budget\";\nimport type { AgentRouteBinding } from \"./agent-routes\";\nimport type { BudgetLimits, BudgetMonth } from \"./schema/budget\";\nimport type { DateClock } from \"./schema/common\";\nimport type { TaskId } from \"./schema/id\";\nimport type { ExecutionReference, Task } from \"./schema/task\";\nimport { routeKey } from \"./agent-routes\";\nimport { InvalidTransitionError } from \"./schema/transitions\";\nimport { RecordNotFoundError, VersionConflictError } from \"./store/driver\";\nimport type { FactoryStores } from \"./store/engine\";\n\n/** Minimal persistence capabilities required to gate and record one dispatch. */\nexport type DispatchStores = Pick<FactoryStores, \"graphs\" | \"receipts\" | \"tasks\">;\n\n/**\n * The provider may have accepted the request, so the running attempt must remain available for\n * recovery with its original launch key. Catchers must not queue a replacement attempt blindly.\n */\nexport class ExecutionLaunchPendingError extends Error {}\n\n/** Stable machine-readable reason a Task could not be dispatched. */\nexport type DispatchRefusalCode =\n | \"route_mismatch\"\n | \"awaiting_approval\"\n | \"approval_denied\"\n | \"dependencies_unsatisfied\"\n | \"dispatch_race\";\n\n/** Non-terminal dispatch refusal that identifies the gated Task and retryable reason. */\nexport class DispatchRefusedError extends Error {\n /** Exact Task whose current dispatch attempt was refused. */\n readonly taskId: TaskId;\n /** Stable gate or race category suitable for branching and telemetry. */\n readonly code: DispatchRefusalCode;\n /** Human-readable refusal context; canonical Task state remains authoritative. */\n readonly reason: string;\n\n constructor(taskId: TaskId, code: DispatchRefusalCode, reason: string) {\n super(`dispatch of \"${taskId}\" refused: ${reason}`);\n this.name = \"DispatchRefusedError\";\n this.taskId = taskId;\n this.code = code;\n this.reason = reason;\n }\n}\n\n/**\n * An exhausted budget is definitive, not a retryable refusal: the task has\n * already been failed and receipted when this throws, so it never re-enters\n * the sweep. Operator retry recovers it once budget frees up.\n */\nexport class BudgetExhaustedError extends Error {\n /** Exact Task already transitioned to `failed` by the exhausted admission. */\n readonly taskId: TaskId;\n /** UTC budget month whose limit refused the reservation. */\n readonly month: BudgetMonth;\n /** Counted settled cost and live reservations observed for that month. */\n readonly spendUsd: number;\n\n constructor(taskId: TaskId, month: BudgetMonth, spendUsd: number) {\n super(`dispatch of \"${taskId}\" refused: budget for ${month} is exhausted at $${spendUsd}`);\n this.name = \"BudgetExhaustedError\";\n this.taskId = taskId;\n this.month = month;\n this.spendUsd = spendUsd;\n }\n}\n\n/** Factory-wide reservation limits and snapshot used to gate one dispatch. */\nexport interface DispatchBudget {\n /** Factory-wide monthly ceiling and amount reserved for each admitted Task. */\n limits: Pick<BudgetLimits, \"totalUsd\" | \"perTaskReservationUsd\">;\n /** Defaults to true. False records reservations without enforcing the monthly ceiling. */\n enforce?: boolean;\n /** Defaults to the current UTC month. */\n month?: BudgetMonth;\n /** Clock used only to derive the reservation month and timestamp. Defaults to `new Date()`. */\n now?: DateClock;\n /** A dispatcher-wide snapshot used to calculate spend without another list. */\n tasks?: readonly Task[];\n}\n\ninterface DispatchContext {\n /** Exact workflow-confirmed continuation attempt; other replacement attempts count as retries. */\n continuationAttempt?: number;\n /**\n * Maximum Task attempt number allowed for launch failure retries. Defaults to 45. A normal\n * failure below the limit requeues the Task, which increments its attempt for the next sweep.\n */\n maxLaunchAttempts?: number;\n /** Reserve-before-execution policy for this dispatch. */\n budget?: DispatchBudget;\n}\n\n/** Exact provider launch or explicit workflow-owned execution for one Task dispatch. */\nexport type DispatchOptions = DispatchContext &\n (\n | {\n /** Required exact binding when the Task declares an agent route. */\n route?: AgentRouteBinding;\n /**\n * Starts the durable execution for the task (an Eve agent run). Runs after\n * the task has won the queued -> running transition. Its provider reference\n * is mandatory so recovery can always find an accepted run.\n */\n launch: (task: Task) => Promise<ExecutionReference>;\n workflowOwned?: never;\n }\n | {\n /**\n * Marks route-less work whose workflow advances the running Task without a provider\n * session. The caller must schedule that workflow after this admission returns.\n */\n workflowOwned: true;\n route?: never;\n launch?: never;\n }\n );\n\n/** Task identity and exact launch mode supplied to one dispatch attempt. */\nexport type DispatchTaskInput = { readonly taskId: TaskId } & DispatchOptions;\n\n// A fresh Vercel project rejects its own agents' sessions until its identity\n// finishes provisioning; at one sweep per minute the budget must outlast that.\n/** Default launch-attempt allowance covering delayed identity provisioning. */\nexport const DEFAULT_MAX_LAUNCH_ATTEMPTS = 45;\n\n/**\n * Claims one queued Task and starts its exact execution mode.\n *\n * @remarks\n * Dispatch first checks route, approval, dependency, and optional budget gates, then atomically\n * transitions the Task from `queued` to `running`. Only the winner invokes `launch`. A successful\n * provider dispatch returns only after its execution reference is stored; it does not mean the\n * Task completed. `workflowOwned: true` returns the claimed running Task without starting a\n * provider session.\n *\n * An ordinary launch failure is rethrown after a best-effort transition back to `queued`, or to\n * `failed` once `maxLaunchAttempts` is exhausted. `ExecutionLaunchPendingError` instead leaves the\n * attempt running because the provider may have accepted the request; recovery must inspect or\n * repeat the launch with the original idempotency key. Budget exhaustion fails and receipts the\n * Task before throwing `BudgetExhaustedError`. Since compensating transitions are best-effort,\n * callers handling any thrown launch error should reread canonical Task state before deciding\n * whether to schedule another attempt.\n *\n * @param stores - Task, graph, and receipt stores that own the dispatch gates.\n * @param input - Exact Task identity plus either a launcher or workflow-owned marker.\n * @returns The running Task, including its durably recorded execution when a launcher was used.\n * @throws `RecordNotFoundError` when the Task is absent.\n * @throws `DispatchRefusedError` when routing, approval, dependencies, or a budget-admission race refuses dispatch.\n * @throws `InvalidTransitionError` when the Task is no longer queued when this caller claims it.\n * @throws `BudgetExhaustedError` after the Task has been failed because no reservation fits.\n * @throws `ExecutionLaunchPendingError` when provider acceptance is ambiguous and requires recovery.\n * @see The shipped `docs/recipes/eve-agent.md` and `docs/recipes/retry-recovery.md` recipes.\n *\n * @example\n * ```ts\n * import {\n * DispatchRefusedError,\n * dispatchTask,\n * type DispatchStores,\n * } from \"@vercel/factory/execution\";\n * import type { TaskId } from \"@vercel/factory/tasks\";\n * import { parseAgentRouteBinding } from \"@vercel/factory/workflows\";\n *\n * declare const stores: DispatchStores;\n * declare const taskId: TaskId;\n * const route = parseAgentRouteBinding({ id: \"worker\", version: 1 });\n *\n * try {\n * await dispatchTask(stores, {\n * taskId,\n * route,\n * launch: async () => ({ provider: \"eve\", sessionId: \"ses_example\" }),\n * });\n * } catch (error) {\n * if (error instanceof DispatchRefusedError) {\n * // `error.code` identifies the gate; canonical Task state remains authoritative.\n * } else {\n * throw error;\n * }\n * }\n * ```\n */\nexport async function dispatchTask(\n stores: DispatchStores,\n input: DispatchTaskInput,\n): Promise<Task> {\n const { taskId } = input;\n const options: DispatchOptions = input;\n const task = await stores.tasks.get(taskId);\n if (task === null) {\n throw new RecordNotFoundError(\"tasks\", taskId);\n }\n const route = task.work.route;\n if (route !== undefined && options.workflowOwned === true) {\n throw new DispatchRefusedError(\n taskId,\n \"route_mismatch\",\n `agent route ${routeKey(route)} requires a launcher`,\n );\n }\n if (\n route !== undefined &&\n (options.route?.id !== route.id || options.route?.version !== route.version)\n ) {\n throw new DispatchRefusedError(\n taskId,\n \"route_mismatch\",\n `launch requires exact agent route ${routeKey(route)}`,\n );\n }\n if (task.approval === \"required\") {\n throw new DispatchRefusedError(taskId, \"awaiting_approval\", \"awaiting human approval\");\n }\n if (task.approval === \"denied\") {\n throw new DispatchRefusedError(taskId, \"approval_denied\", \"approval was denied\");\n }\n if (\n !(await stores.graphs.dependenciesSatisfied({\n rootTaskId: task.rootTaskId,\n taskId: task.id,\n }))\n ) {\n throw new DispatchRefusedError(\n taskId,\n \"dependencies_unsatisfied\",\n \"graph dependencies must all be succeeded before dispatch\",\n );\n }\n\n // The reservation gates queued -> running only; any other state falls\n // through to the transition below, which rejects it as invalid.\n let reservation: { reservedUsd: number; reservedAt: string } | undefined;\n if (options.budget !== undefined && task.state === \"queued\") {\n const budgetNow = (options.budget.now ?? (() => new Date()))().toISOString();\n const month = options.budget.month ?? budgetMonthOf(budgetNow);\n // This task's own retained reservation from a prior attempt is excluded,\n // because re-reserving overwrites it. Its settled cost stays counted:\n // that money was actually spent and a retry must not reopen it.\n const rows = (\n options.budget.enforce === false ? [] : (options.budget.tasks ?? (await stores.tasks.list()))\n ).map((row) => (row.id === taskId ? { ...row, reservedUsd: undefined } : row));\n const spendUsd = computeMonthSpendUsd(rows, month);\n const { totalUsd, perTaskReservationUsd } = options.budget.limits;\n if (options.budget.enforce !== false && !canReserve(spendUsd, options.budget.limits)) {\n const reason = `budget exhausted: ${month} spend $${spendUsd} cannot fit a $${perTaskReservationUsd} reservation under $${totalUsd}`;\n // Fail first, so BudgetExhaustedError's contract (task already failed)\n // holds; losing the task to a concurrent dispatcher surfaces as a\n // refusal, never as a false exhaustion with a misleading receipt.\n try {\n await stores.tasks.transition(taskId, \"failed\", { reason, expectFrom: \"queued\" });\n } catch (error) {\n if (error instanceof InvalidTransitionError || error instanceof VersionConflictError) {\n throw new DispatchRefusedError(\n taskId,\n \"dispatch_race\",\n \"another dispatcher took the task\",\n );\n }\n throw error;\n }\n // Best-effort: the failed transition above already wrote its own\n // receipt carrying this reason, so losing this supplementary one\n // cannot lose the evidence.\n await stores.receipts\n .append({\n repositoryIds: task.repositoryIds,\n taskId,\n kind: task.kind,\n state: \"reservation_refused\",\n phase: \"budget\",\n reason,\n attempt: task.attempt,\n dedupeKey: `task:${taskId}:reservation_refused:${task.attempt}`,\n })\n .catch(() => undefined);\n throw new BudgetExhaustedError(taskId, month, spendUsd);\n }\n reservation = { reservedUsd: perTaskReservationUsd, reservedAt: budgetNow };\n }\n\n const running = await stores.tasks.transition(taskId, \"running\", {\n expectFrom: \"queued\",\n expectAttempt: task.attempt,\n ...(reservation === undefined\n ? {}\n : {\n patch: {\n reservedUsd: reservation.reservedUsd,\n reservedAt: reservation.reservedAt,\n },\n }),\n ...(task.attempt > 1\n ? { phase: options.continuationAttempt === task.attempt ? \"workflow\" : \"retry\" }\n : {}),\n });\n if (options.workflowOwned === true) return running;\n let execution: ExecutionReference;\n try {\n execution = await options.launch(running);\n } catch (error) {\n if (error instanceof ExecutionLaunchPendingError) throw error;\n const reason = (error instanceof Error ? error.message : String(error)).slice(0, 500);\n const limit = options.maxLaunchAttempts ?? DEFAULT_MAX_LAUNCH_ATTEMPTS;\n if (running.attempt < limit) {\n const retryReason = `launch failed (attempt ${running.attempt} of ${limit}), will retry: ${reason}`;\n await stores.tasks\n .transition(taskId, \"queued\", {\n reason: retryReason,\n phase: \"retry\",\n expectFrom: \"running\",\n expectAttempt: running.attempt,\n })\n .catch(() => undefined);\n } else {\n await stores.tasks\n .transition(taskId, \"failed\", {\n reason: `launch failed after ${running.attempt} attempts: ${reason}`,\n phase: \"retry\",\n expectFrom: \"running\",\n expectAttempt: running.attempt,\n })\n .catch(() => undefined);\n }\n throw error;\n }\n // Surface a failed binding so a durable caller can recover the same launch;\n // never report acceptance with an unrecorded or stale execution.\n return stores.tasks.recordExecution(taskId, {\n execution,\n expectAttempt: running.attempt,\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dispatch.mjs","names":[],"sources":["../src/dispatch.ts"],"sourcesContent":["import { budgetTasks } from \"./budget\";\nimport { budgetMonthOf, canReserve, computeMonthSpendUsd } from \"./budget\";\nimport type { AgentRouteBinding } from \"./agent-routes\";\nimport type { BudgetLimits, BudgetMonth } from \"./schema/budget\";\nimport type { DateClock } from \"./schema/common\";\nimport type { TaskId } from \"./schema/id\";\nimport type { ExecutionReference, Task } from \"./schema/task\";\nimport { routeKey } from \"./agent-routes\";\nimport { InvalidTransitionError } from \"./schema/transitions\";\nimport { RecordNotFoundError, VersionConflictError } from \"./store/driver\";\nimport type { FactoryStores } from \"./store/engine\";\n\n/** Minimal persistence capabilities required to gate and record one dispatch. */\nexport type DispatchStores = Pick<FactoryStores, \"graphs\" | \"receipts\" | \"tasks\">;\n\n/**\n * The provider may have accepted the request, so the running attempt must remain available for\n * recovery with its original launch key. Catchers must not queue a replacement attempt blindly.\n */\nexport class ExecutionLaunchPendingError extends Error {}\n\n/** Stable machine-readable reason a Task could not be dispatched. */\nexport type DispatchRefusalCode =\n | \"route_mismatch\"\n | \"awaiting_approval\"\n | \"approval_denied\"\n | \"dependencies_unsatisfied\"\n | \"dispatch_race\";\n\n/** Non-terminal dispatch refusal that identifies the gated Task and retryable reason. */\nexport class DispatchRefusedError extends Error {\n /** Exact Task whose current dispatch attempt was refused. */\n readonly taskId: TaskId;\n /** Stable gate or race category suitable for branching and telemetry. */\n readonly code: DispatchRefusalCode;\n /** Human-readable refusal context; canonical Task state remains authoritative. */\n readonly reason: string;\n\n constructor(taskId: TaskId, code: DispatchRefusalCode, reason: string) {\n super(`dispatch of \"${taskId}\" refused: ${reason}`);\n this.name = \"DispatchRefusedError\";\n this.taskId = taskId;\n this.code = code;\n this.reason = reason;\n }\n}\n\n/**\n * An exhausted budget is definitive, not a retryable refusal: the task has\n * already been failed and receipted when this throws, so it never re-enters\n * the sweep. Operator retry recovers it once budget frees up.\n */\nexport class BudgetExhaustedError extends Error {\n /** Exact Task already transitioned to `failed` by the exhausted admission. */\n readonly taskId: TaskId;\n /** UTC budget month whose limit refused the reservation. */\n readonly month: BudgetMonth;\n /** Counted settled cost and live reservations observed for that month. */\n readonly spendUsd: number;\n\n constructor(taskId: TaskId, month: BudgetMonth, spendUsd: number) {\n super(`dispatch of \"${taskId}\" refused: budget for ${month} is exhausted at $${spendUsd}`);\n this.name = \"BudgetExhaustedError\";\n this.taskId = taskId;\n this.month = month;\n this.spendUsd = spendUsd;\n }\n}\n\n/** Factory-wide reservation limits and snapshot used to gate one dispatch. */\nexport interface DispatchBudget {\n /** Factory-wide monthly ceiling and amount reserved for each admitted Task. */\n limits: Pick<BudgetLimits, \"totalUsd\" | \"perTaskReservationUsd\">;\n /** Defaults to true. False records reservations without enforcing the monthly ceiling. */\n enforce?: boolean;\n /** Defaults to the current UTC month. */\n month?: BudgetMonth;\n /** Clock used only to derive the reservation month and timestamp. Defaults to `new Date()`. */\n now?: DateClock;\n /** A dispatcher-wide snapshot used to calculate spend without another list. */\n tasks?: readonly Task[];\n}\n\ninterface DispatchContext {\n /** Exact workflow-confirmed continuation attempt; other replacement attempts count as retries. */\n continuationAttempt?: number;\n /**\n * Maximum Task attempt number allowed for launch failure retries. Defaults to 45. A normal\n * failure below the limit requeues the Task, which increments its attempt for the next sweep.\n */\n maxLaunchAttempts?: number;\n /** Reserve-before-execution policy for this dispatch. */\n budget?: DispatchBudget;\n}\n\n/** Exact provider launch or explicit workflow-owned execution for one Task dispatch. */\nexport type DispatchOptions = DispatchContext &\n (\n | {\n /** Required exact binding when the Task declares an agent route. */\n route?: AgentRouteBinding;\n /**\n * Starts the durable execution for the task (an Eve agent run). Runs after\n * the task has won the queued -> running transition. Its provider reference\n * is mandatory so recovery can always find an accepted run.\n */\n launch: (task: Task) => Promise<ExecutionReference>;\n workflowOwned?: never;\n }\n | {\n /**\n * Marks route-less work whose workflow advances the running Task without a provider\n * session. The caller must schedule that workflow after this admission returns.\n */\n workflowOwned: true;\n route?: never;\n launch?: never;\n }\n );\n\n/** Task identity and exact launch mode supplied to one dispatch attempt. */\nexport type DispatchTaskInput = { readonly taskId: TaskId } & DispatchOptions;\n\n// A fresh Vercel project rejects its own agents' sessions until its identity\n// finishes provisioning; at one sweep per minute the budget must outlast that.\n/** Default launch-attempt allowance covering delayed identity provisioning. */\nexport const DEFAULT_MAX_LAUNCH_ATTEMPTS = 45;\n\n/**\n * Claims one queued Task and starts its exact execution mode.\n *\n * @remarks\n * Dispatch first checks route, approval, dependency, and optional budget gates, then atomically\n * transitions the Task from `queued` to `running`. Only the winner invokes `launch`. A successful\n * provider dispatch returns only after its execution reference is stored; it does not mean the\n * Task completed. `workflowOwned: true` returns the claimed running Task without starting a\n * provider session.\n *\n * An ordinary launch failure is rethrown after a best-effort transition back to `queued`, or to\n * `failed` once `maxLaunchAttempts` is exhausted. `ExecutionLaunchPendingError` instead leaves the\n * attempt running because the provider may have accepted the request; recovery must inspect or\n * repeat the launch with the original idempotency key. Budget exhaustion fails and receipts the\n * Task before throwing `BudgetExhaustedError`. Since compensating transitions are best-effort,\n * callers handling any thrown launch error should reread canonical Task state before deciding\n * whether to schedule another attempt.\n *\n * @param stores - Task, graph, and receipt stores that own the dispatch gates.\n * @param input - Exact Task identity plus either a launcher or workflow-owned marker.\n * @returns The running Task, including its durably recorded execution when a launcher was used.\n * @throws `RecordNotFoundError` when the Task is absent.\n * @throws `DispatchRefusedError` when routing, approval, dependencies, or a budget-admission race refuses dispatch.\n * @throws `InvalidTransitionError` when the Task is no longer queued when this caller claims it.\n * @throws `BudgetExhaustedError` after the Task has been failed because no reservation fits.\n * @throws `ExecutionLaunchPendingError` when provider acceptance is ambiguous and requires recovery.\n * @see The shipped `docs/recipes/eve-agent.md` and `docs/recipes/retry-recovery.md` recipes.\n *\n * @example\n * ```ts\n * import {\n * DispatchRefusedError,\n * dispatchTask,\n * type DispatchStores,\n * } from \"@vercel/factory/execution\";\n * import type { TaskId } from \"@vercel/factory/tasks\";\n * import { parseAgentRouteBinding } from \"@vercel/factory/workflows\";\n *\n * declare const stores: DispatchStores;\n * declare const taskId: TaskId;\n * const route = parseAgentRouteBinding({ id: \"worker\", version: 1 });\n *\n * try {\n * await dispatchTask(stores, {\n * taskId,\n * route,\n * launch: async () => ({ provider: \"eve\", sessionId: \"ses_example\" }),\n * });\n * } catch (error) {\n * if (error instanceof DispatchRefusedError) {\n * // `error.code` identifies the gate; canonical Task state remains authoritative.\n * } else {\n * throw error;\n * }\n * }\n * ```\n */\nexport async function dispatchTask(\n stores: DispatchStores,\n input: DispatchTaskInput,\n): Promise<Task> {\n const { taskId } = input;\n const options: DispatchOptions = input;\n const task = await stores.tasks.get(taskId);\n if (task === null) {\n throw new RecordNotFoundError(\"tasks\", taskId);\n }\n const route = task.work.route;\n if (route !== undefined && options.workflowOwned === true) {\n throw new DispatchRefusedError(\n taskId,\n \"route_mismatch\",\n `agent route ${routeKey(route)} requires a launcher`,\n );\n }\n if (\n route !== undefined &&\n (options.route?.id !== route.id || options.route?.version !== route.version)\n ) {\n throw new DispatchRefusedError(\n taskId,\n \"route_mismatch\",\n `launch requires exact agent route ${routeKey(route)}`,\n );\n }\n if (task.approval === \"required\") {\n throw new DispatchRefusedError(taskId, \"awaiting_approval\", \"awaiting human approval\");\n }\n if (task.approval === \"denied\") {\n throw new DispatchRefusedError(taskId, \"approval_denied\", \"approval was denied\");\n }\n if (\n !(await stores.graphs.dependenciesSatisfied({\n rootTaskId: task.rootTaskId,\n taskId: task.id,\n }))\n ) {\n throw new DispatchRefusedError(\n taskId,\n \"dependencies_unsatisfied\",\n \"graph dependencies must all be succeeded before dispatch\",\n );\n }\n\n // The reservation gates queued -> running only; any other state falls\n // through to the transition below, which rejects it as invalid.\n let reservation: { reservedUsd: number; reservedAt: string } | undefined;\n if (options.budget !== undefined && task.state === \"queued\") {\n const budgetNow = (options.budget.now ?? (() => new Date()))().toISOString();\n const month = options.budget.month ?? budgetMonthOf(budgetNow);\n // This task's own retained reservation from a prior attempt is excluded,\n // because re-reserving overwrites it. Its settled cost stays counted:\n // that money was actually spent and a retry must not reopen it.\n const rows = (\n options.budget.enforce === false ? [] : (options.budget.tasks ?? (await budgetTasks(stores)))\n ).map((row) => (row.id === taskId ? { ...row, reservedUsd: undefined } : row));\n const spendUsd = computeMonthSpendUsd(rows, month);\n const { totalUsd, perTaskReservationUsd } = options.budget.limits;\n if (options.budget.enforce !== false && !canReserve(spendUsd, options.budget.limits)) {\n const reason = `budget exhausted: ${month} spend $${spendUsd} cannot fit a $${perTaskReservationUsd} reservation under $${totalUsd}`;\n // Fail first, so BudgetExhaustedError's contract (task already failed)\n // holds; losing the task to a concurrent dispatcher surfaces as a\n // refusal, never as a false exhaustion with a misleading receipt.\n try {\n await stores.tasks.transition(taskId, \"failed\", { reason, expectFrom: \"queued\" });\n } catch (error) {\n if (error instanceof InvalidTransitionError || error instanceof VersionConflictError) {\n throw new DispatchRefusedError(\n taskId,\n \"dispatch_race\",\n \"another dispatcher took the task\",\n );\n }\n throw error;\n }\n // Best-effort: the failed transition above already wrote its own\n // receipt carrying this reason, so losing this supplementary one\n // cannot lose the evidence.\n await stores.receipts\n .append({\n repositoryIds: task.repositoryIds,\n taskId,\n kind: task.kind,\n state: \"reservation_refused\",\n phase: \"budget\",\n reason,\n attempt: task.attempt,\n dedupeKey: `task:${taskId}:reservation_refused:${task.attempt}`,\n })\n .catch(() => undefined);\n throw new BudgetExhaustedError(taskId, month, spendUsd);\n }\n reservation = { reservedUsd: perTaskReservationUsd, reservedAt: budgetNow };\n }\n\n const running = await stores.tasks.transition(taskId, \"running\", {\n expectFrom: \"queued\",\n expectAttempt: task.attempt,\n ...(reservation === undefined\n ? {}\n : {\n patch: {\n reservedUsd: reservation.reservedUsd,\n reservedAt: reservation.reservedAt,\n },\n }),\n ...(task.attempt > 1\n ? { phase: options.continuationAttempt === task.attempt ? \"workflow\" : \"retry\" }\n : {}),\n });\n if (options.workflowOwned === true) return running;\n let execution: ExecutionReference;\n try {\n execution = await options.launch(running);\n } catch (error) {\n if (error instanceof ExecutionLaunchPendingError) throw error;\n const reason = (error instanceof Error ? error.message : String(error)).slice(0, 500);\n const limit = options.maxLaunchAttempts ?? DEFAULT_MAX_LAUNCH_ATTEMPTS;\n if (running.attempt < limit) {\n const retryReason = `launch failed (attempt ${running.attempt} of ${limit}), will retry: ${reason}`;\n await stores.tasks\n .transition(taskId, \"queued\", {\n reason: retryReason,\n phase: \"retry\",\n expectFrom: \"running\",\n expectAttempt: running.attempt,\n })\n .catch(() => undefined);\n } else {\n await stores.tasks\n .transition(taskId, \"failed\", {\n reason: `launch failed after ${running.attempt} attempts: ${reason}`,\n phase: \"retry\",\n expectFrom: \"running\",\n expectAttempt: running.attempt,\n })\n .catch(() => undefined);\n }\n throw error;\n }\n // Surface a failed binding so a durable caller can recover the same launch;\n // never report acceptance with an unrecorded or stale execution.\n return stores.tasks.recordExecution(taskId, {\n execution,\n expectAttempt: running.attempt,\n });\n}\n"],"mappings":"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"}
|
package/dist/eve/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Signal, SignalOutcome } from "../schema/signal.mjs";
|
|
2
1
|
import { ChangeId, RepositoryIds, SignalId, TaskId } from "../schema/id.mjs";
|
|
3
2
|
import { JsonValue } from "../schema/work.mjs";
|
|
4
3
|
import { Task, TaskQuestion } from "../schema/task.mjs";
|
|
5
4
|
import { MaybePromise } from "../schema/common.mjs";
|
|
6
5
|
import { Receipt, ReceiptKind, ReceiptState } from "../schema/receipt.mjs";
|
|
6
|
+
import { Signal, SignalOutcome } from "../schema/signal.mjs";
|
|
7
7
|
import { AnswerQuestionResult, FactoryStores } from "../store/engine.mjs";
|
|
8
8
|
import { EveTranscriptMaterializer, EveTranscriptMaterializerOptions, EveTranscriptRequestScope, TranscriptMaterializationInput, createEveTranscriptMaterializer } from "./transcript.mjs";
|
|
9
9
|
import { ConversationBinding, FactoryConversationSessionBinding, conversationBinding, conversationBindingForSession, conversationHeaders, factoryReplyAddressAttribute, factoryReplyAddressHeader, factoryReplyChannelAttribute, factoryReplyChannelHeader, factoryRepositoryIdsAttribute, factoryRepositoryIdsHeader, factoryTaskAttemptAttribute, factoryTaskAttemptHeader, factoryTaskAttribute, factoryTaskHeader, hasFactoryTaskBinding, optionalTaskIdForSession, replyToForSession, repositoryIdsForSession, requireTaskIdForSession, taskBindingForSession, taskHeaders, taskSessionAuth, withFactoryTask, withOptionalFactoryTask } from "./task-session.mjs";
|