@stage5/lumine 0.2.67 → 0.2.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/lib/admin-featured-history.js +185 -0
- package/lib/admin-featured.js +559 -0
- package/lib/admin-runtime-logs.js +23 -3
- package/lib/admin-workflows.js +54 -4
- package/lib/admin.js +371 -101
- package/lib/commands.js +19 -8
- package/lib/constants.js +21 -0
- package/lib/sdk.js +6 -0
- package/lib/sponsor-duty.js +15 -2
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +68 -6
- package/sdk/LUMINE_ADMIN.md +542 -48
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
fsyncSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { requestJson } from "./http.js";
|
|
11
|
+
import { readAdminJsonFile, writeAdminJsonFile } from "./admin-news.js";
|
|
12
|
+
import {
|
|
13
|
+
acquireCheckpointLock,
|
|
14
|
+
releaseCheckpointLock,
|
|
15
|
+
} from "./admin-workflows.js";
|
|
16
|
+
|
|
17
|
+
const MAX_BYTES = 16 * 1024 * 1024;
|
|
18
|
+
const BASE = "/cli/admin/subjects/featured";
|
|
19
|
+
const hashBytes = (value) => createHash("sha256").update(value).digest("hex");
|
|
20
|
+
const canonical = (value) =>
|
|
21
|
+
Array.isArray(value)
|
|
22
|
+
? value.map(canonical)
|
|
23
|
+
: value && typeof value === "object"
|
|
24
|
+
? Object.fromEntries(
|
|
25
|
+
Object.keys(value)
|
|
26
|
+
.sort()
|
|
27
|
+
.map((key) => [key, canonical(value[key])]),
|
|
28
|
+
)
|
|
29
|
+
: value;
|
|
30
|
+
export const featuredFingerprint = (value) =>
|
|
31
|
+
hashBytes(JSON.stringify(canonical(value)));
|
|
32
|
+
|
|
33
|
+
function invalid(message, details) {
|
|
34
|
+
const error = new Error(message);
|
|
35
|
+
error.code = "CLI_ADMIN_FEATURED_WORKFLOW_INVALID";
|
|
36
|
+
if (details)
|
|
37
|
+
error.data = {
|
|
38
|
+
ok: false,
|
|
39
|
+
status: "partial_failure",
|
|
40
|
+
error: {
|
|
41
|
+
code: error.code,
|
|
42
|
+
message,
|
|
43
|
+
details,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
return error;
|
|
47
|
+
}
|
|
48
|
+
function positive(value, label) {
|
|
49
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
50
|
+
throw invalid(`${label} must be a positive integer.`);
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
function save(file, value) {
|
|
54
|
+
writeAdminJsonFile(file, value, { privateFile: true, maxBytes: MAX_BYTES });
|
|
55
|
+
const fd = openSync(file, "r");
|
|
56
|
+
try {
|
|
57
|
+
fsyncSync(fd);
|
|
58
|
+
} finally {
|
|
59
|
+
closeSync(fd);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function readApprovedFeaturedPlan(file, approval) {
|
|
64
|
+
const raw = readAdminJsonFile(file, "the prepared Featured plan", {
|
|
65
|
+
maxBytes: MAX_BYTES,
|
|
66
|
+
});
|
|
67
|
+
const { plan, planHash } = raw.data || raw;
|
|
68
|
+
if (
|
|
69
|
+
!plan ||
|
|
70
|
+
plan.schemaVersion !== 1 ||
|
|
71
|
+
!Array.isArray(plan.finalIds) ||
|
|
72
|
+
!Array.isArray(plan.expectedIds) ||
|
|
73
|
+
!planHash ||
|
|
74
|
+
featuredFingerprint(plan) !== planHash ||
|
|
75
|
+
approval !== planHash
|
|
76
|
+
) {
|
|
77
|
+
throw invalid(
|
|
78
|
+
"Read the prepared plan, obtain Mikey's go-ahead, then pass --approve <exact-plan-hash>. Modified plans cannot be applied.",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
positive(plan.id, "Plan ID");
|
|
82
|
+
return { plan, planHash };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function readFeaturedSelections(file) {
|
|
86
|
+
const raw = readAdminJsonFile(file, "agent-selected comment decisions", {
|
|
87
|
+
maxBytes: 2 * 1024 * 1024,
|
|
88
|
+
});
|
|
89
|
+
positive(raw?.reviewId, "Review ID");
|
|
90
|
+
positive(raw?.coverageId, "Reviewed coverage ID");
|
|
91
|
+
if (!Array.isArray(raw.selections) || raw.selections.length > 20_000) {
|
|
92
|
+
throw invalid(
|
|
93
|
+
"selections must be an array of at most 20000 agent-selected comments.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
const selections = raw.selections.map((item) => {
|
|
98
|
+
positive(item?.commentId, "Comment ID");
|
|
99
|
+
positive(item?.pageId, "Page receipt ID");
|
|
100
|
+
if (seen.has(item.commentId))
|
|
101
|
+
throw invalid("Duplicate selected comment ID.");
|
|
102
|
+
seen.add(item.commentId);
|
|
103
|
+
if (
|
|
104
|
+
item.anyoneCanReward !== undefined &&
|
|
105
|
+
typeof item.anyoneCanReward !== "boolean"
|
|
106
|
+
) {
|
|
107
|
+
throw invalid("anyoneCanReward must be a boolean.");
|
|
108
|
+
}
|
|
109
|
+
const anyoneCanReward = item.anyoneCanReward === true;
|
|
110
|
+
const rewardTwinkles = item.rewardTwinkles ?? 0;
|
|
111
|
+
if (
|
|
112
|
+
![0, 3].includes(rewardTwinkles) ||
|
|
113
|
+
(rewardTwinkles && !anyoneCanReward)
|
|
114
|
+
) {
|
|
115
|
+
throw invalid(
|
|
116
|
+
"rewardTwinkles must be 0, or 3 with anyoneCanReward=true.",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
commentId: item.commentId,
|
|
121
|
+
pageId: item.pageId,
|
|
122
|
+
anyoneCanReward,
|
|
123
|
+
rewardTwinkles,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
return { reviewId: raw.reviewId, coverageId: raw.coverageId, selections };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function verifyPageFiles(state, checkpoint) {
|
|
130
|
+
if (!state.review) {
|
|
131
|
+
if (Object.keys(state.subjects || {}).length)
|
|
132
|
+
throw invalid("Checkpoint subjects have no review snapshot.");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const ids = state.review.subjects.map((item) => String(item.id)).sort();
|
|
136
|
+
if (
|
|
137
|
+
JSON.stringify(ids) !==
|
|
138
|
+
JSON.stringify(Object.keys(state.subjects || {}).sort())
|
|
139
|
+
) {
|
|
140
|
+
throw invalid(
|
|
141
|
+
"The checkpoint omits or adds Subjects relative to its review snapshot.",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
for (const subject of Object.values(state.subjects || {})) {
|
|
145
|
+
const manifest = state.review.subjects.find(
|
|
146
|
+
(item) => item.id === subject.id,
|
|
147
|
+
);
|
|
148
|
+
if (!manifest || manifest.snapshotMaxId !== subject.snapshotMaxId)
|
|
149
|
+
throw invalid("Subject boundary changed.");
|
|
150
|
+
const verified = {
|
|
151
|
+
id: subject.id,
|
|
152
|
+
snapshotMaxId: subject.snapshotMaxId,
|
|
153
|
+
pages: [],
|
|
154
|
+
lastPageId: null,
|
|
155
|
+
commentsFetched: 0,
|
|
156
|
+
complete: false,
|
|
157
|
+
lastCommentId: subject.snapshotMaxId + 1,
|
|
158
|
+
};
|
|
159
|
+
let previous = null;
|
|
160
|
+
for (const entry of subject.pages || []) {
|
|
161
|
+
if (
|
|
162
|
+
entry.file !==
|
|
163
|
+
`${checkpoint}.page-${positive(entry.id, "Page ID")}.json`
|
|
164
|
+
) {
|
|
165
|
+
throw invalid("A page file does not belong to this checkpoint.");
|
|
166
|
+
}
|
|
167
|
+
const bytes = readFileSync(entry.file);
|
|
168
|
+
if (bytes.length > MAX_BYTES || hashBytes(bytes) !== entry.sha256)
|
|
169
|
+
throw invalid(
|
|
170
|
+
"A downloaded page changed or is incomplete; do not claim review coverage.",
|
|
171
|
+
);
|
|
172
|
+
const result = JSON.parse(bytes.toString("utf8"));
|
|
173
|
+
assertPageResult(result, state, verified);
|
|
174
|
+
const page = result.data?.page;
|
|
175
|
+
if (
|
|
176
|
+
page?.id !== entry.id ||
|
|
177
|
+
page.reviewId !== state.review.id ||
|
|
178
|
+
page.subjectId !== subject.id ||
|
|
179
|
+
page.previousPageId !== previous ||
|
|
180
|
+
page.snapshotMaxId !== subject.snapshotMaxId
|
|
181
|
+
)
|
|
182
|
+
throw invalid("Page receipt chain does not match this review.");
|
|
183
|
+
previous = page.id;
|
|
184
|
+
verified.pages.push(entry);
|
|
185
|
+
verified.lastPageId = page.id;
|
|
186
|
+
verified.commentsFetched = page.commentsRead;
|
|
187
|
+
verified.complete = page.exhausted;
|
|
188
|
+
verified.lastCommentId =
|
|
189
|
+
result.data.comments.at(-1)?.id ?? verified.lastCommentId;
|
|
190
|
+
}
|
|
191
|
+
if (
|
|
192
|
+
subject.lastPageId !== previous ||
|
|
193
|
+
subject.complete !== verified.complete ||
|
|
194
|
+
subject.commentsFetched !== verified.commentsFetched ||
|
|
195
|
+
subject.lastCommentId !== verified.lastCommentId
|
|
196
|
+
) {
|
|
197
|
+
throw invalid("Checkpoint progress does not match its downloaded pages.");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function assertPageResult(result, state, subject) {
|
|
203
|
+
const page = result?.data?.page;
|
|
204
|
+
positive(page?.id, "Canonical page ID");
|
|
205
|
+
const comments = result.data.comments;
|
|
206
|
+
if (
|
|
207
|
+
result.ok !== true ||
|
|
208
|
+
page.reviewId !== state.review.id ||
|
|
209
|
+
page.subjectId !== subject.id ||
|
|
210
|
+
page.snapshotMaxId !== subject.snapshotMaxId ||
|
|
211
|
+
page.previousPageId !== subject.lastPageId ||
|
|
212
|
+
page.pages !== subject.pages.length + 1 ||
|
|
213
|
+
!Array.isArray(comments) ||
|
|
214
|
+
page.commentsRead !== subject.commentsFetched + comments.length ||
|
|
215
|
+
typeof page.exhausted !== "boolean" ||
|
|
216
|
+
(page.exhausted ? page.nextCursor !== null : !page.nextCursor) ||
|
|
217
|
+
(!page.exhausted && !comments.length) ||
|
|
218
|
+
comments.some(
|
|
219
|
+
(item) =>
|
|
220
|
+
!Number.isSafeInteger(item.id) ||
|
|
221
|
+
item.id <= 0 ||
|
|
222
|
+
item.id > subject.snapshotMaxId,
|
|
223
|
+
) ||
|
|
224
|
+
new Set(comments.map((item) => item.id)).size !== comments.length
|
|
225
|
+
) {
|
|
226
|
+
throw invalid(
|
|
227
|
+
"The API did not confirm the exact continuing comment snapshot.",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (
|
|
231
|
+
comments.some(
|
|
232
|
+
(item, index) =>
|
|
233
|
+
item.id >= (index ? comments[index - 1].id : subject.lastCommentId),
|
|
234
|
+
)
|
|
235
|
+
) {
|
|
236
|
+
throw invalid(
|
|
237
|
+
"Comment pages repeated rows or did not advance in canonical order.",
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return page;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function runFeaturedWorkflow({
|
|
244
|
+
options,
|
|
245
|
+
operation,
|
|
246
|
+
authToken,
|
|
247
|
+
runId,
|
|
248
|
+
request = requestJson,
|
|
249
|
+
}) {
|
|
250
|
+
const apiUrl = String(options.apiUrl).replace(/\/$/, "");
|
|
251
|
+
const call = async (suffix, body, key) =>
|
|
252
|
+
request({
|
|
253
|
+
method: body === undefined ? "GET" : "POST",
|
|
254
|
+
url: `${apiUrl}${BASE}${suffix}`,
|
|
255
|
+
authToken,
|
|
256
|
+
body,
|
|
257
|
+
timeoutMs: options.timeoutMs,
|
|
258
|
+
headers: {
|
|
259
|
+
"x-lumine-admin-run-id": String(runId),
|
|
260
|
+
...(key ? { "x-lumine-idempotency-key": key } : {}),
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
if (operation.featuredWorkflow === "apply") {
|
|
264
|
+
const { plan, planHash } = readApprovedFeaturedPlan(
|
|
265
|
+
options.adminFile,
|
|
266
|
+
options.adminApprove,
|
|
267
|
+
);
|
|
268
|
+
if (plan.runId !== runId)
|
|
269
|
+
throw invalid(
|
|
270
|
+
"This plan belongs to a different run; prepare and approve a fresh plan.",
|
|
271
|
+
);
|
|
272
|
+
const result = await call(
|
|
273
|
+
"/plan/apply",
|
|
274
|
+
{ planId: plan.id, approvedHash: planHash },
|
|
275
|
+
`cli:featured-apply:${runId}:${planHash.slice(0, 32)}`,
|
|
276
|
+
);
|
|
277
|
+
let listed;
|
|
278
|
+
try {
|
|
279
|
+
listed = await call("");
|
|
280
|
+
} catch (error) {
|
|
281
|
+
throw invalid(
|
|
282
|
+
"The server returned an apply receipt, but the final board read failed. Inspect the receipt and retry the same approved plan; do not assume no change occurred.",
|
|
283
|
+
{
|
|
284
|
+
canonicalResult: result,
|
|
285
|
+
expectedFinalIds: plan.finalIds,
|
|
286
|
+
verificationError: error.message,
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
const actual = listed?.data?.subjects?.map((item) => item.id);
|
|
291
|
+
if (
|
|
292
|
+
listed?.ok !== true ||
|
|
293
|
+
result?.ok !== true ||
|
|
294
|
+
result.data?.appliedPlanId !== plan.id ||
|
|
295
|
+
result.data?.approvedHash !== planHash ||
|
|
296
|
+
JSON.stringify(actual) !== JSON.stringify(plan.finalIds)
|
|
297
|
+
) {
|
|
298
|
+
throw invalid(
|
|
299
|
+
"The approved plan's final board could not be verified. Do not restore or overwrite intervening changes.",
|
|
300
|
+
{
|
|
301
|
+
canonicalResult: result,
|
|
302
|
+
liveBoard: listed,
|
|
303
|
+
expectedFinalIds: plan.finalIds,
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return { ...result, data: { ...result.data, verifiedFinalIds: actual } };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const workflow = operation.featuredWorkflow;
|
|
311
|
+
if (!options.adminCheckpoint)
|
|
312
|
+
throw invalid(
|
|
313
|
+
"Pass --checkpoint <private-file.json> for resumable Featured work.",
|
|
314
|
+
);
|
|
315
|
+
const checkpoint = path.resolve(options.adminCheckpoint);
|
|
316
|
+
const selection =
|
|
317
|
+
workflow === "recommend" ? readFeaturedSelections(options.adminFile) : null;
|
|
318
|
+
const fingerprint = featuredFingerprint({
|
|
319
|
+
apiUrl,
|
|
320
|
+
runId,
|
|
321
|
+
kind: selection ? "recommend" : "scan",
|
|
322
|
+
selection,
|
|
323
|
+
});
|
|
324
|
+
const lock = acquireCheckpointLock(checkpoint, fingerprint);
|
|
325
|
+
try {
|
|
326
|
+
let state;
|
|
327
|
+
if (
|
|
328
|
+
options.adminResume ||
|
|
329
|
+
workflow === "acknowledge" ||
|
|
330
|
+
workflow === "report"
|
|
331
|
+
) {
|
|
332
|
+
state = readAdminJsonFile(checkpoint, "the Featured checkpoint", {
|
|
333
|
+
maxBytes: MAX_BYTES,
|
|
334
|
+
});
|
|
335
|
+
if (
|
|
336
|
+
state.schemaVersion !== 1 ||
|
|
337
|
+
state.fingerprint !== fingerprint ||
|
|
338
|
+
state.runId !== runId ||
|
|
339
|
+
state.apiUrl !== apiUrl
|
|
340
|
+
) {
|
|
341
|
+
throw invalid(
|
|
342
|
+
"This checkpoint belongs to a different run, server, or selection. Resume the exact original operation.",
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
if (!selection) verifyPageFiles(state, checkpoint);
|
|
346
|
+
} else {
|
|
347
|
+
if (existsSync(checkpoint))
|
|
348
|
+
throw invalid(
|
|
349
|
+
"The checkpoint already exists. Use --resume or choose a new file.",
|
|
350
|
+
);
|
|
351
|
+
state = {
|
|
352
|
+
schemaVersion: 1,
|
|
353
|
+
fingerprint,
|
|
354
|
+
apiUrl,
|
|
355
|
+
runId,
|
|
356
|
+
requestId: `cli:featured-review:${randomUUID()}`,
|
|
357
|
+
subjects: {},
|
|
358
|
+
completed: {},
|
|
359
|
+
};
|
|
360
|
+
save(checkpoint, state);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (workflow === "recommend") {
|
|
364
|
+
for (const item of selection.selections) {
|
|
365
|
+
if (state.completed[item.commentId]) continue;
|
|
366
|
+
try {
|
|
367
|
+
const result = await call(
|
|
368
|
+
`/reviews/${selection.reviewId}/recommendations/${item.commentId}`,
|
|
369
|
+
{
|
|
370
|
+
...item,
|
|
371
|
+
coverageId: selection.coverageId,
|
|
372
|
+
},
|
|
373
|
+
`cli:featured-rec:${runId}:${fingerprint.slice(0, 24)}:${item.commentId}`,
|
|
374
|
+
);
|
|
375
|
+
const outcome = result?.data?.encouragement;
|
|
376
|
+
if (
|
|
377
|
+
result?.ok !== true ||
|
|
378
|
+
outcome?.commentId !== item.commentId ||
|
|
379
|
+
outcome?.reviewId !== selection.reviewId ||
|
|
380
|
+
outcome?.pageId !== item.pageId ||
|
|
381
|
+
outcome?.coverageId !== selection.coverageId ||
|
|
382
|
+
outcome?.anyoneCanRewardRequested !== item.anyoneCanReward ||
|
|
383
|
+
outcome?.rewardTwinklesRequested !== item.rewardTwinkles
|
|
384
|
+
) {
|
|
385
|
+
throw invalid(
|
|
386
|
+
"The API did not confirm this exact comment decision.",
|
|
387
|
+
{ canonicalResult: result },
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
state.completed[item.commentId] = outcome;
|
|
391
|
+
save(checkpoint, state);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
error.featuredProgress = {
|
|
394
|
+
checkpointPath: checkpoint,
|
|
395
|
+
completedCount: Object.keys(state.completed).length,
|
|
396
|
+
failedCommentId: item.commentId,
|
|
397
|
+
targetCount: selection.selections.length,
|
|
398
|
+
};
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const report = await call(`/reviews/${selection.reviewId}/report`);
|
|
403
|
+
return {
|
|
404
|
+
ok: true,
|
|
405
|
+
status: "success",
|
|
406
|
+
changed: Object.values(state.completed).some((item) => item.changed),
|
|
407
|
+
data: {
|
|
408
|
+
batch: {
|
|
409
|
+
targetCount: selection.selections.length,
|
|
410
|
+
completedCount: Object.keys(state.completed).length,
|
|
411
|
+
checkpointPath: checkpoint,
|
|
412
|
+
outcomes: state.completed,
|
|
413
|
+
},
|
|
414
|
+
featuredReport: report.data,
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (!state.review) {
|
|
420
|
+
const result = await call("/reviews", {}, state.requestId);
|
|
421
|
+
const review = result?.data?.review;
|
|
422
|
+
positive(review?.id, "Canonical review ID");
|
|
423
|
+
if (
|
|
424
|
+
result.ok !== true ||
|
|
425
|
+
review.runId !== runId ||
|
|
426
|
+
!Array.isArray(review.subjects) ||
|
|
427
|
+
review.subjects.length > 100 ||
|
|
428
|
+
new Set(review.subjects.map((item) => item.id)).size !==
|
|
429
|
+
review.subjects.length
|
|
430
|
+
) {
|
|
431
|
+
throw invalid(
|
|
432
|
+
"The API did not return a complete bounded Featured review snapshot.",
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
state.review = review;
|
|
436
|
+
for (const subject of review.subjects) {
|
|
437
|
+
positive(subject.id, "Subject ID");
|
|
438
|
+
if (
|
|
439
|
+
!Number.isSafeInteger(subject.snapshotMaxId) ||
|
|
440
|
+
subject.snapshotMaxId < 0
|
|
441
|
+
)
|
|
442
|
+
throw invalid("Invalid comment boundary.");
|
|
443
|
+
state.subjects[subject.id] = {
|
|
444
|
+
...subject,
|
|
445
|
+
pages: [],
|
|
446
|
+
lastPageId: null,
|
|
447
|
+
lastCommentId: subject.snapshotMaxId + 1,
|
|
448
|
+
commentsFetched: 0,
|
|
449
|
+
complete: false,
|
|
450
|
+
blocked: null,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
save(checkpoint, state);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (workflow === "scan") {
|
|
457
|
+
for (const subject of Object.values(state.subjects)) {
|
|
458
|
+
subject.blocked = null;
|
|
459
|
+
while (!subject.complete) {
|
|
460
|
+
if (subject.pages.length >= 100_000)
|
|
461
|
+
throw invalid("Page safety limit reached; coverage is incomplete.");
|
|
462
|
+
try {
|
|
463
|
+
const result = await call(
|
|
464
|
+
`/reviews/${state.review.id}/pages`,
|
|
465
|
+
{
|
|
466
|
+
subjectId: subject.id,
|
|
467
|
+
previousPageId: subject.lastPageId,
|
|
468
|
+
},
|
|
469
|
+
`cli:featured-page:${runId}:${state.review.id}:${subject.id}:${subject.lastPageId || 0}`,
|
|
470
|
+
);
|
|
471
|
+
const page = assertPageResult(result, state, subject);
|
|
472
|
+
const file = `${checkpoint}.page-${page.id}.json`;
|
|
473
|
+
save(file, result);
|
|
474
|
+
subject.pages.push({
|
|
475
|
+
id: page.id,
|
|
476
|
+
file,
|
|
477
|
+
sha256: hashBytes(readFileSync(file)),
|
|
478
|
+
});
|
|
479
|
+
subject.lastPageId = page.id;
|
|
480
|
+
subject.commentsFetched = page.commentsRead;
|
|
481
|
+
subject.lastCommentId =
|
|
482
|
+
result.data.comments.at(-1)?.id ?? subject.lastCommentId;
|
|
483
|
+
subject.complete = page.exhausted;
|
|
484
|
+
save(checkpoint, state);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
const code = error?.data?.error?.code || error.code;
|
|
487
|
+
if (
|
|
488
|
+
![
|
|
489
|
+
"CLI_ADMIN_SECRET_REVEAL_REQUIRED",
|
|
490
|
+
"CLI_ADMIN_SUBJECT_NOT_FOUND",
|
|
491
|
+
"CLI_ADMIN_NOT_FOUND",
|
|
492
|
+
].includes(code)
|
|
493
|
+
) {
|
|
494
|
+
error.featuredProgress = {
|
|
495
|
+
checkpointPath: checkpoint,
|
|
496
|
+
failedSubjectId: subject.id,
|
|
497
|
+
subjectsCompleted: Object.values(state.subjects).filter(
|
|
498
|
+
(item) => item.complete,
|
|
499
|
+
).length,
|
|
500
|
+
};
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
subject.blocked = { code, message: error.message };
|
|
504
|
+
save(checkpoint, state);
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
} else if (workflow === "acknowledge") {
|
|
510
|
+
if (!options.adminReviewed)
|
|
511
|
+
throw invalid("Read every downloaded page before passing --reviewed.");
|
|
512
|
+
const pageIds = Object.values(state.subjects)
|
|
513
|
+
.filter((item) => item.complete)
|
|
514
|
+
.map((item) => item.lastPageId);
|
|
515
|
+
const result = await call(
|
|
516
|
+
`/reviews/${state.review.id}/coverage`,
|
|
517
|
+
{ pageIds, reviewed: true },
|
|
518
|
+
`cli:featured-covered:${runId}:${featuredFingerprint({ reviewId: state.review.id, pageIds }).slice(0, 32)}`,
|
|
519
|
+
);
|
|
520
|
+
if (
|
|
521
|
+
result?.ok !== true ||
|
|
522
|
+
result.data?.coverage?.reviewId !== state.review.id ||
|
|
523
|
+
result.data?.coverage?.reviewed !== true
|
|
524
|
+
) {
|
|
525
|
+
throw invalid("The API did not confirm reviewed coverage.");
|
|
526
|
+
}
|
|
527
|
+
state.coverage = result.data.coverage;
|
|
528
|
+
save(checkpoint, state);
|
|
529
|
+
} else if (workflow === "report") {
|
|
530
|
+
return call(`/reviews/${state.review.id}/report`);
|
|
531
|
+
}
|
|
532
|
+
const subjects = Object.values(state.subjects);
|
|
533
|
+
return {
|
|
534
|
+
ok: true,
|
|
535
|
+
status: "success",
|
|
536
|
+
changed: false,
|
|
537
|
+
data: {
|
|
538
|
+
review: state.review,
|
|
539
|
+
checkpointPath: checkpoint,
|
|
540
|
+
fetched: {
|
|
541
|
+
complete: subjects.every((item) => item.complete),
|
|
542
|
+
subjectsCompleted: subjects.filter((item) => item.complete).length,
|
|
543
|
+
commentsFetched: subjects.reduce(
|
|
544
|
+
(sum, item) => sum + item.commentsFetched,
|
|
545
|
+
0,
|
|
546
|
+
),
|
|
547
|
+
blocked: subjects
|
|
548
|
+
.filter((item) => item.blocked)
|
|
549
|
+
.map((item) => ({ subjectId: item.id, ...item.blocked })),
|
|
550
|
+
},
|
|
551
|
+
// Downloading is not a claim that the agent read the content.
|
|
552
|
+
reviewedCoverage: state.coverage || null,
|
|
553
|
+
pageFiles: subjects.flatMap((item) => item.pages),
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
} finally {
|
|
557
|
+
releaseCheckpointLock(lock);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
@@ -141,7 +141,7 @@ function startIntentPath(options) {
|
|
|
141
141
|
);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
function readStartIntent(intentPath, apiUrl) {
|
|
144
|
+
function readStartIntent(intentPath, apiUrl, host = "primary") {
|
|
145
145
|
if (!existsSync(intentPath)) return null;
|
|
146
146
|
const intent = readAdminJsonFile(
|
|
147
147
|
intentPath,
|
|
@@ -156,6 +156,11 @@ function readStartIntent(intentPath, apiUrl) {
|
|
|
156
156
|
) {
|
|
157
157
|
return null;
|
|
158
158
|
}
|
|
159
|
+
if ((intent.host || "primary") !== host) {
|
|
160
|
+
throw validationError(
|
|
161
|
+
`A ${intent.host || "primary"} runtime-log start has an unresolved outcome. Replay that host's start before choosing ${host}; its request key was preserved.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
159
164
|
return intent;
|
|
160
165
|
}
|
|
161
166
|
|
|
@@ -185,6 +190,7 @@ function createSession({ options, apiUrl, review }) {
|
|
|
185
190
|
kind: "lumine-admin-runtime-log-review",
|
|
186
191
|
apiUrl,
|
|
187
192
|
reviewId: review.id,
|
|
193
|
+
...(review.ownerHostId ? { ownerHostId: review.ownerHostId } : {}),
|
|
188
194
|
leaseToken: review.leaseToken,
|
|
189
195
|
status: review.status,
|
|
190
196
|
outputDirectory,
|
|
@@ -581,10 +587,14 @@ export async function runAdminRuntimeLogWorkflow({
|
|
|
581
587
|
}) {
|
|
582
588
|
const apiUrl = normalizeApiUrl(options.apiUrl);
|
|
583
589
|
if (operation.runtimeLogAction === "start") {
|
|
590
|
+
const host = operation.runtimeLogHost || "primary";
|
|
591
|
+
if (!["primary", "target"].includes(host)) {
|
|
592
|
+
throw validationError("Invalid runtime-log host.");
|
|
593
|
+
}
|
|
584
594
|
const intentPath = startIntentPath(options);
|
|
585
595
|
const intent = options.idempotencyKey
|
|
586
596
|
? null
|
|
587
|
-
: readStartIntent(intentPath, apiUrl);
|
|
597
|
+
: readStartIntent(intentPath, apiUrl, host);
|
|
588
598
|
const startRequestId = intent?.requestId || requestId;
|
|
589
599
|
if (!intent) {
|
|
590
600
|
writeAdminJsonFile(
|
|
@@ -592,6 +602,7 @@ export async function runAdminRuntimeLogWorkflow({
|
|
|
592
602
|
{
|
|
593
603
|
kind: "lumine-admin-runtime-log-review-start",
|
|
594
604
|
apiUrl,
|
|
605
|
+
host,
|
|
595
606
|
requestId: startRequestId,
|
|
596
607
|
createdAt: new Date().toISOString(),
|
|
597
608
|
},
|
|
@@ -602,7 +613,7 @@ export async function runAdminRuntimeLogWorkflow({
|
|
|
602
613
|
try {
|
|
603
614
|
result = await requestJson({
|
|
604
615
|
method: "POST",
|
|
605
|
-
url: `${apiUrl}/cli/admin/runtime-logs/reviews`,
|
|
616
|
+
url: `${apiUrl}/cli/admin/runtime-logs${operation.runtimeLogHost ? `/hosts/${host}` : ""}/reviews`,
|
|
606
617
|
authToken,
|
|
607
618
|
body: {},
|
|
608
619
|
headers: requestHeaders({ requestId: startRequestId }),
|
|
@@ -631,6 +642,15 @@ export async function runAdminRuntimeLogWorkflow({
|
|
|
631
642
|
throw error;
|
|
632
643
|
}
|
|
633
644
|
const review = validateReview(result?.data?.review);
|
|
645
|
+
if (
|
|
646
|
+
operation.runtimeLogHost &&
|
|
647
|
+
(!/^i-[a-f0-9]{17}$/.test(review.ownerHostId || "") ||
|
|
648
|
+
review.ownerHostRole !== host)
|
|
649
|
+
) {
|
|
650
|
+
throw validationError(
|
|
651
|
+
"The API did not confirm a host-owned review. Preserve the start intent and recover it; do not clear logs.",
|
|
652
|
+
);
|
|
653
|
+
}
|
|
634
654
|
const session = createSession({ options, apiUrl, review });
|
|
635
655
|
// The session now holds the lease; a later rerun must start fresh.
|
|
636
656
|
clearStartIntent(intentPath);
|