@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.
@@ -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);