@hue-run/sdk 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -79,6 +79,7 @@ export declare class EvaluationClient {
79
79
  }): Promise<Uint8Array>;
80
80
  /** Stage bytes at the storage capability Hue issued. The Hue key is never sent to storage. */
81
81
  uploadArtifactBytes(upload: ArtifactUpload, bytes: Uint8Array, contentType: string): Promise<void>;
82
+ private putArtifact;
82
83
  private page;
83
84
  private registryPage;
84
85
  /** Reads the current project to confirm the key and origin. */
@@ -106,10 +107,14 @@ export declare class EvaluationClient {
106
107
  * `files` given as bytes or a path is checked against the artifact policy and uploaded as a
107
108
  * verified artifact first, under a key derived from the version, the case's `externalKey`, the
108
109
  * role and the bytes, so sending the same case again reuses the artifact. Every file and the
109
- * case itself are checked before the first upload.
110
+ * case itself are checked before the first upload. A file given by `path` is hashed a slice at
111
+ * a time when it is checked and read again when it is uploaded, one file at a time, so no more
112
+ * than one file is held in memory; only bytes that match the checked SHA-256 are sent.
110
113
  *
111
114
  * @throws TypeError or RangeError for `files` outside the artifact policy, or a case Hue would
112
- * refuse for its fields or size, before any request.
115
+ * refuse for its fields or size, before any request. TypeError when a file given by `path`
116
+ * changed after it was checked, or can no longer be read, before any of its bytes are sent: it
117
+ * is not staged and the case is not written.
113
118
  */
114
119
  addCase(id: string, input: CaseWrite): Promise<{
115
120
  /** The stored case. */
@@ -402,17 +402,29 @@ export class EvaluationClient {
402
402
  }
403
403
  }
404
404
  /** Stage bytes at the storage capability Hue issued. The Hue key is never sent to storage. */
405
- async uploadArtifactBytes(upload, bytes, contentType) {
405
+ uploadArtifactBytes(upload, bytes, contentType) {
406
+ return this.putArtifact(upload, contentType, bytes);
407
+ }
408
+ /** @internal Stage `byteSize` bytes read from `body` a slice at a time, without the copy a
409
+ * byte body is given; the request declares their length, as it does for bytes. */
410
+ uploadArtifactStream(upload, body, byteSize, contentType) {
411
+ return this.putArtifact(upload, contentType, body, byteSize);
412
+ }
413
+ async putArtifact(upload, contentType, body, byteSize) {
406
414
  const uploadUrl = signedUploadUrl(upload.uploadUrl);
407
415
  if (upload.method !== "PUT")
408
416
  throw new HueApiError();
409
417
  const headers = signedUploadHeaders(upload.headers, contentType);
418
+ // Storage refuses a body of unknown length, which a stream would otherwise be sent as.
419
+ if (byteSize !== undefined)
420
+ headers["content-length"] = String(byteSize);
410
421
  let response;
411
422
  try {
412
423
  response = await fetch(uploadUrl, {
413
424
  method: "PUT",
414
425
  headers,
415
- body: bytes,
426
+ body,
427
+ ...(byteSize !== undefined ? { duplex: "half" } : {}),
416
428
  redirect: "error",
417
429
  signal: AbortSignal.timeout(Math.max(this.timeoutMillis, 120_000)),
418
430
  });
@@ -480,10 +492,14 @@ export class EvaluationClient {
480
492
  * `files` given as bytes or a path is checked against the artifact policy and uploaded as a
481
493
  * verified artifact first, under a key derived from the version, the case's `externalKey`, the
482
494
  * role and the bytes, so sending the same case again reuses the artifact. Every file and the
483
- * case itself are checked before the first upload.
495
+ * case itself are checked before the first upload. A file given by `path` is hashed a slice at
496
+ * a time when it is checked and read again when it is uploaded, one file at a time, so no more
497
+ * than one file is held in memory; only bytes that match the checked SHA-256 are sent.
484
498
  *
485
499
  * @throws TypeError or RangeError for `files` outside the artifact policy, or a case Hue would
486
- * refuse for its fields or size, before any request.
500
+ * refuse for its fields or size, before any request. TypeError when a file given by `path`
501
+ * changed after it was checked, or can no longer be read, before any of its bytes are sent: it
502
+ * is not staged and the case is not written.
487
503
  */
488
504
  async addCase(id, input) {
489
505
  // The canonical spelling: the reservation keys of uploaded files name the version.
@@ -426,15 +426,22 @@ export async function uploadOutputFiles(client, executionId, files, save, timing
426
426
  }
427
427
  /** Reserve, stage and verify one artifact; its ID once Hue verified it. Replaying the key settles
428
428
  * on the same artifact, including one Hue finished verifying after an earlier attempt stopped. */
429
- async function publishArtifact(client, declared, bytes, label, timing) {
429
+ async function publishArtifact(client, declared, body, label, timing) {
430
430
  const reserved = await client.reserveArtifact(declared);
431
431
  let state = reserved.state;
432
432
  if (state !== "ready") {
433
433
  // A verification already running holds the uploaded bytes; Hue refuses another upload then.
434
434
  if (reserved.copyState !== "acknowledged" && state !== "verifying") {
435
+ // A checked file is read again, whole, only now and only if it still has the bytes its
436
+ // check hashed: a file that changed sends nothing.
437
+ const bytes = body instanceof Uint8Array ? body : await checkedBytes(body);
435
438
  const upload = await client.requestArtifactUpload(reserved.id);
436
439
  try {
437
- await client.uploadArtifactBytes(upload, bytes, declared.contentType);
440
+ if (body instanceof Uint8Array)
441
+ await client.uploadArtifactBytes(upload, bytes, declared.contentType);
442
+ // Sent from the write's own buffer a slice at a time, never copied whole.
443
+ else
444
+ await client.uploadArtifactStream(upload, slicesOf(bytes), bytes.byteLength, declared.contentType);
438
445
  }
439
446
  catch {
440
447
  // A lost staging acknowledgement or an immutable object already present can only be
@@ -447,6 +454,59 @@ async function publishArtifact(client, declared, bytes, label, timing) {
447
454
  throw new Error(`${label} was not verified by Hue (${state})`);
448
455
  return reserved.id;
449
456
  }
457
+ /** How much of a case file given by path its check reads at once, to hash it. */
458
+ const CASE_FILE_SLICE = 1024 * 1024;
459
+ /** Bytes as a stream of 1 MiB views of them, made as the request reads them. */
460
+ function slicesOf(bytes) {
461
+ let offset = 0;
462
+ return new ReadableStream({
463
+ pull(controller) {
464
+ if (offset >= bytes.byteLength)
465
+ return controller.close();
466
+ const end = Math.min(bytes.byteLength, offset + CASE_FILE_SLICE);
467
+ controller.enqueue(bytes.subarray(offset, end));
468
+ offset = end;
469
+ },
470
+ }, { highWaterMark: 0 });
471
+ }
472
+ /**
473
+ * The bytes of a checked case file, read whole when it is uploaded: case files are uploaded one
474
+ * at a time, into one buffer the write reuses, so no more than one is held in memory. Only bytes
475
+ * with the checked size and SHA-256 are returned, so the bytes sent are the bytes the check
476
+ * hashed; a file that changed after its check, or can no longer be read, is refused with a
477
+ * `TypeError` before anything is sent. The bytes are valid until the next file is read.
478
+ */
479
+ async function checkedBytes(file) {
480
+ const changed = () => new TypeError(`${file.label} changed after it was checked`);
481
+ const unreadable = (error) => new TypeError(`${file.label} could not be read`, { cause: error });
482
+ const handle = await open(file.path, "r").catch((error) => {
483
+ throw unreadable(error);
484
+ });
485
+ try {
486
+ const read = (buffer, offset) => handle
487
+ .read(buffer, offset, buffer.byteLength - offset, null)
488
+ .then(({ bytesRead }) => bytesRead)
489
+ .catch((error) => {
490
+ throw unreadable(error);
491
+ });
492
+ // Never more than its checked size and one byte, the byte that shows it grew.
493
+ if ((file.scratch.buffer?.byteLength ?? 0) < file.byteSize)
494
+ file.scratch.buffer = Buffer.allocUnsafe(file.byteSize);
495
+ const bytes = file.scratch.buffer.subarray(0, file.byteSize);
496
+ for (let offset = 0; offset < bytes.byteLength;) {
497
+ const bytesRead = await read(bytes, offset);
498
+ if (!bytesRead)
499
+ throw changed();
500
+ offset += bytesRead;
501
+ }
502
+ if ((await read(Buffer.alloc(1), 0)) || sha256(bytes) !== file.sha256)
503
+ throw changed();
504
+ return bytes;
505
+ }
506
+ finally {
507
+ await handle.close().catch(() => undefined);
508
+ }
509
+ }
450
510
  /** Roles a pinned case input can carry. */
451
511
  const caseFileRoles = [
452
512
  ...targetFileRoles,
@@ -466,8 +526,9 @@ function caseFileName(name) {
466
526
  throw new TypeError("A case file needs a well-formed filename of 1–255 UTF-16 code units without path separators or control characters");
467
527
  return name;
468
528
  }
469
- /** The bytes of a case file given by path: a regular file within the size limit, read once. */
470
- async function caseFileBytes(path, label) {
529
+ /** A case file given by path, checked: a regular file within the size limit, hashed a slice at a
530
+ * time so it is never held whole. Its emptiness is the caller's check, as for bytes. */
531
+ async function checkedFile(path, label, scratch) {
471
532
  if (typeof path !== "string" || !path)
472
533
  throw new TypeError(`${label} needs bytes or a path`);
473
534
  // A path with no file behind it, or one that cannot be read, is refused as any invalid file.
@@ -479,9 +540,32 @@ async function caseFileBytes(path, label) {
479
540
  throw new TypeError(`${label} is not a regular file`);
480
541
  if (info.size > outputFileLimits.bytes)
481
542
  throw new RangeError(`${label} exceeds 25 MiB`);
482
- return readFile(path).catch((error) => {
543
+ const handle = await open(path, "r").catch((error) => {
483
544
  throw unreadable(error);
484
545
  });
546
+ try {
547
+ const hash = createHash("sha256");
548
+ const slice = Buffer.allocUnsafe(CASE_FILE_SLICE);
549
+ let byteSize = 0;
550
+ for (;;) {
551
+ const { bytesRead } = await handle
552
+ .read(slice, 0, slice.byteLength, null)
553
+ .catch((error) => {
554
+ throw unreadable(error);
555
+ });
556
+ if (!bytesRead)
557
+ break;
558
+ byteSize += bytesRead;
559
+ // A file that grew past the limit since its size was read is refused, not read whole.
560
+ if (byteSize > outputFileLimits.bytes)
561
+ throw new RangeError(`${label} exceeds 25 MiB`);
562
+ hash.update(slice.subarray(0, bytesRead));
563
+ }
564
+ return { path, label, byteSize, sha256: hash.digest("hex"), scratch };
565
+ }
566
+ finally {
567
+ await handle.close().catch(() => undefined);
568
+ }
485
569
  }
486
570
  /** A stand-in for an artifact a case file is uploaded as, the length of a real ID. */
487
571
  const STAND_IN_ID = "00000000-0000-4000-8000-000000000000";
@@ -502,6 +586,7 @@ export async function caseFileReferences(client, versionId, input, beforeUpload
502
586
  throw new RangeError(`A case pins at most ${MAX_CASE_FILES} files`);
503
587
  const prepared = [];
504
588
  const seen = new Set();
589
+ const scratch = {};
505
590
  for (const [index, file] of files.entries()) {
506
591
  const label = `Case file ${index + 1}`;
507
592
  if (!file || typeof file !== "object")
@@ -530,18 +615,21 @@ export async function caseFileReferences(client, versionId, input, beforeUpload
530
615
  throw new TypeError(`${label} names bytes and a path; give one`);
531
616
  if (file.bytes !== undefined && !(file.bytes instanceof Uint8Array))
532
617
  throw new TypeError(`${label} bytes must be a Uint8Array`);
533
- const bytes = file.bytes ?? (await caseFileBytes(file.path, label));
534
- if (!bytes.byteLength)
618
+ // Bytes are kept as given; a path is kept as its size and digest, and read again to upload.
619
+ const body = file.bytes ?? (await checkedFile(file.path, label, scratch));
620
+ const byteSize = body instanceof Uint8Array ? body.byteLength : body.byteSize;
621
+ if (!byteSize)
535
622
  throw new RangeError(`${label} is empty`);
536
- if (bytes.byteLength > outputFileLimits.bytes)
623
+ if (byteSize > outputFileLimits.bytes)
537
624
  throw new RangeError(`${label} exceeds 25 MiB`);
538
- const digest = sha256(bytes);
625
+ const digest = body instanceof Uint8Array ? sha256(body) : body.sha256;
539
626
  prepared.push({
540
627
  role: file.role,
541
628
  filename,
542
629
  contentType: file.contentType,
543
- bytes,
630
+ byteSize,
544
631
  sha256: digest,
632
+ body,
545
633
  });
546
634
  identity = `file:${digest}:${file.contentType}:${filename}`;
547
635
  }
@@ -574,9 +662,9 @@ export async function caseFileReferences(client, versionId, input, beforeUpload
574
662
  idempotencyKey: `hue-sdk:case:${versionId}:${caseKey}:${item.role}:${item.sha256}:${sha256(Buffer.from(item.filename)).slice(0, 16)}:${sha256(Buffer.from(item.contentType)).slice(0, 16)}`,
575
663
  filename: item.filename,
576
664
  contentType: item.contentType,
577
- byteSize: item.bytes.byteLength,
665
+ byteSize: item.byteSize,
578
666
  sha256: item.sha256,
579
- }, item.bytes, `Case file ${item.filename}`, timing);
667
+ }, item.body, `Case file ${item.filename}`, timing);
580
668
  pin({ artifactId, role: item.role });
581
669
  }
582
670
  return references;
@@ -38,8 +38,12 @@ export declare class MediaFetchError extends Error {
38
38
  /** HTTP status of the answer that failed, when there was one. */
39
39
  status?: number | undefined,
40
40
  /** Seconds Hue asked the caller to wait, on `unavailable`, when it said. */
41
- retryAfterSeconds?: number | undefined, options?: {
41
+ retryAfterSeconds?: number | undefined,
42
+ /** `cause`, the error behind a failure; `snapshot`, set when a snapshot's copy was read, so a
43
+ * `forbidden` message names the Read and write key it needs. */
44
+ options?: {
42
45
  cause?: unknown;
46
+ snapshot?: boolean;
43
47
  });
44
48
  }
45
49
  /** One recorded file, downloaded and checked against its digest. */
@@ -11,6 +11,8 @@ const messages = {
11
11
  digest_mismatch: "The downloaded bytes do not match the requested SHA-256",
12
12
  failed: "The media request failed",
13
13
  };
14
+ /** A snapshot's copies take only a Read and write key, where a trace's files take a Read key too. */
15
+ const snapshotForbidden = "The key cannot read a snapshot's files; use a Read and write key";
14
16
  /** Thrown by {@link MediaClient.fetch}. The message is fixed and never includes response text. */
15
17
  export class MediaFetchError extends Error {
16
18
  reason;
@@ -22,8 +24,16 @@ export class MediaFetchError extends Error {
22
24
  /** HTTP status of the answer that failed, when there was one. */
23
25
  status,
24
26
  /** Seconds Hue asked the caller to wait, on `unavailable`, when it said. */
25
- retryAfterSeconds, options) {
26
- super(reason === "failed" && status ? `${messages.failed} (HTTP ${status})` : messages[reason], options);
27
+ retryAfterSeconds,
28
+ /** `cause`, the error behind a failure; `snapshot`, set when a snapshot's copy was read, so a
29
+ * `forbidden` message names the Read and write key it needs. */
30
+ options) {
31
+ const { snapshot, ...rest } = options ?? {};
32
+ super(reason === "failed" && status
33
+ ? `${messages.failed} (HTTP ${status})`
34
+ : reason === "forbidden" && snapshot
35
+ ? snapshotForbidden
36
+ : messages[reason], rest);
27
37
  this.reason = reason;
28
38
  this.status = status;
29
39
  this.retryAfterSeconds = retryAfterSeconds;
@@ -354,7 +364,9 @@ export class MediaClient {
354
364
  }
355
365
  if (status === 401 || status === 403) {
356
366
  await response.body?.cancel().catch(() => undefined);
357
- throw new MediaFetchError("forbidden", status);
367
+ throw new MediaFetchError("forbidden", status, undefined, {
368
+ snapshot: path.startsWith("/trace-snapshots/"),
369
+ });
358
370
  }
359
371
  if (!response.ok) {
360
372
  await response.body?.cancel().catch(() => undefined);
@@ -186,9 +186,10 @@ export interface CaseFileReference {
186
186
  }
187
187
  /**
188
188
  * A pinned input file for {@link CaseWrite.files}: a {@link CaseFileReference}, or a file the
189
- * client uploads as a verified artifact, from `bytes` or read once from `path`. An uploaded file
190
- * must have one of `outputContentTypes`, a well-formed name of 1–255 UTF-16 code units without
191
- * path separators or control characters, and 1 byte to 25 MiB.
189
+ * client uploads as a verified artifact, from `bytes` or read from `path`: hashed a slice at a
190
+ * time when the case is checked, then read again as it is uploaded, one file at a time. An
191
+ * uploaded file must have one of `outputContentTypes`, a well-formed name of 1–255 UTF-16 code
192
+ * units without path separators or control characters, and 1 byte to 25 MiB.
192
193
  */
193
194
  export type CaseFileWrite = (CaseFileReference & {
194
195
  /** Not set on an artifact reference: the artifact keeps its own name. */
@@ -27,6 +27,9 @@ export interface InlineFileBytes {
27
27
  bytes: Buffer;
28
28
  decoded: boolean;
29
29
  urlMediaType?: string;
30
+ /** The bytes were decoded from text that reads as base64 by its alphabet alone, not from a
31
+ * `data:` URL or a field its convention declares base64. */
32
+ bareBase64?: boolean;
30
33
  }
31
34
  /**
32
35
  * The bytes an inline file part carries, the file's own bytes whatever its media type: a `data:`
@@ -137,9 +140,15 @@ export declare function reencodeMedia(media: MediaValue): void;
137
140
  /** Whether a found file has bytes to place: nonempty, and a whole string's `data:` URL decodes. */
138
141
  export declare function placeable(file: InlineFilePart, decoded: InlineFileBytes): boolean;
139
142
  /** Whether an uploaded file keeps a 16 KiB prefix of its content in place (text media in a
140
- * provider part, whose entry names the content) rather than its digest. A whole string and a
141
- * file inside a JSON-encoded string always take the digest form. */
142
- export declare function keepsPrefix(file: InlineFilePart, textual: boolean): boolean;
143
+ * provider part, whose entry names the content) rather than its digest. A whole string, a file
144
+ * inside a JSON-encoded string, and a text file a part holds as bare base64 (a GenAI `blob`,
145
+ * Vertex `media` or AI SDK part, whose content is not the text itself) take the digest form. */
146
+ export declare function keepsPrefix(file: InlineFilePart, textual: boolean, decoded: InlineFileBytes): boolean;
147
+ /** A file's bytes: a field its convention declares base64 decoded, any other content read as
148
+ * Hue's reader reads it, whatever the part's media type (`inlineFileBytes`): a `data:` URL by its
149
+ * own encoding, text that reads as base64 decoded (what the GenAI conventions, and
150
+ * `client.media.resolve`, write in a `blob` or `media` part for every media type), anything else
151
+ * as its UTF-8. */
143
152
  export declare function mediaBytes(file: InlineFilePart): InlineFileBytes;
144
153
  export declare function mediaContentType(file: InlineFilePart, decoded: InlineFileBytes): string;
145
154
  export interface FileDigest {
@@ -140,6 +140,7 @@ export function inlineFileBytes(content) {
140
140
  bytes: decoded ?? Buffer.from(content, "utf8"),
141
141
  decoded: decoded !== undefined,
142
142
  ...(mediaType ? { urlMediaType: mediaType } : {}),
143
+ ...(decoded !== undefined && !url ? { bareBase64: true } : {}),
143
144
  };
144
145
  }
145
146
  const audioTypes = {
@@ -711,18 +712,20 @@ export function placeable(file, decoded) {
711
712
  return decoded.bytes.byteLength > 0 && (!file.whole || decoded.decoded);
712
713
  }
713
714
  /** Whether an uploaded file keeps a 16 KiB prefix of its content in place (text media in a
714
- * provider part, whose entry names the content) rather than its digest. A whole string and a
715
- * file inside a JSON-encoded string always take the digest form. */
716
- export function keepsPrefix(file, textual) {
717
- return textual && !file.whole && !file.encoded?.length;
718
- }
715
+ * provider part, whose entry names the content) rather than its digest. A whole string, a file
716
+ * inside a JSON-encoded string, and a text file a part holds as bare base64 (a GenAI `blob`,
717
+ * Vertex `media` or AI SDK part, whose content is not the text itself) take the digest form. */
718
+ export function keepsPrefix(file, textual, decoded) {
719
+ return textual && !file.whole && !file.encoded?.length && !decoded.bareBase64;
720
+ }
721
+ /** A file's bytes: a field its convention declares base64 decoded, any other content read as
722
+ * Hue's reader reads it, whatever the part's media type (`inlineFileBytes`): a `data:` URL by its
723
+ * own encoding, text that reads as base64 decoded (what the GenAI conventions, and
724
+ * `client.media.resolve`, write in a `blob` or `media` part for every media type), anything else
725
+ * as its UTF-8. */
719
726
  export function mediaBytes(file) {
720
727
  if (file.base64)
721
728
  return { bytes: Buffer.from(file.content, "base64"), decoded: true };
722
- if (isMediaContentType(file.contentType) &&
723
- isTextMediaType(file.contentType) &&
724
- !file.content.startsWith("data:"))
725
- return { bytes: Buffer.from(file.content, "utf8"), decoded: false };
726
729
  return inlineFileBytes(file.content);
727
730
  }
728
731
  export function mediaContentType(file, decoded) {
package/dist/setup/cli.js CHANGED
@@ -24,6 +24,10 @@ const extensions = new Map([
24
24
  "mcp",
25
25
  async () => (await loadCommand("mcp", () => import("../cli/mcp.js"))).runMcpCommand(process.argv.slice(3)),
26
26
  ],
27
+ [
28
+ "import",
29
+ async () => (await loadCommand("import", () => import("../cli/import.js"))).runImportCommand(process.argv.slice(3)),
30
+ ],
27
31
  [
28
32
  "listen",
29
33
  async () => (await loadCommand("listen", () => import("../cli/listen.js"))).runListenCommand(process.argv.slice(3)),
@@ -70,7 +74,7 @@ async function main() {
70
74
  }
71
75
  catch {
72
76
  if (!agentRequested) {
73
- process.stderr.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
77
+ process.stderr.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen|import> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
74
78
  return 2;
75
79
  }
76
80
  const event = {
@@ -101,7 +105,7 @@ async function main() {
101
105
  process.stdout.write(`${renderJsonlEvent(event)}\n`);
102
106
  return 2;
103
107
  }
104
- process.stdout.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
108
+ process.stdout.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen|import> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
105
109
  return 0;
106
110
  }
107
111
  const command = parsed.positionals[0];
@@ -133,7 +137,7 @@ async function main() {
133
137
  process.stdout.write(`${renderJsonlEvent(event)}\n`);
134
138
  }
135
139
  else
136
- process.stderr.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
140
+ process.stderr.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen|import> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
137
141
  return 2;
138
142
  }
139
143
  const mode = selectSetupOutputMode({
@@ -57,6 +57,9 @@ export declare class HueTransport {
57
57
  * over the request limit), reported on the trace's root span as `hue.sdk.dropped_records` when
58
58
  * the root is exported, so Hue reads the trace as incomplete by that many records. */
59
59
  private droppedByTrace;
60
+ /** Whether a trace's root carries `hue.sdk.dropped_records`; off for a replay of recorded
61
+ * spans, which must send the same root on every run. */
62
+ private marksDroppedRecords;
60
63
  private instrumentationFailures;
61
64
  private diagnosticPending;
62
65
  private lastDiagnosticAt;
package/dist/transport.js CHANGED
@@ -184,6 +184,9 @@ export class HueTransport {
184
184
  * over the request limit), reported on the trace's root span as `hue.sdk.dropped_records` when
185
185
  * the root is exported, so Hue reads the trace as incomplete by that many records. */
186
186
  droppedByTrace = new Map();
187
+ /** Whether a trace's root carries `hue.sdk.dropped_records`; off for a replay of recorded
188
+ * spans, which must send the same root on every run. */
189
+ marksDroppedRecords = true;
187
190
  instrumentationFailures = 0;
188
191
  diagnosticPending = false;
189
192
  lastDiagnosticAt = -Infinity;
@@ -822,13 +825,20 @@ export class HueTransport {
822
825
  * (`consumeDroppedRecords`): a root written again under retry says the same, and a root whose
823
826
  * request failed has not told Hue, so its count is not forgotten with it. */
824
827
  withDroppedRecords(span) {
825
- if (span.parentSpanContext)
828
+ if (span.parentSpanContext || !this.marksDroppedRecords)
826
829
  return span;
827
830
  const count = this.droppedByTrace.get(span.spanContext().traceId);
828
831
  if (!count)
829
832
  return span;
830
833
  return { ...span, attributes: { ...span.attributes, [DROPPED_RECORDS_KEY]: count } };
831
834
  }
835
+ /** @internal For an import that replays recorded spans: roots never carry the count of
836
+ * records this run did not deliver, so a later run that delivers them sends each root as it
837
+ * was first sent, which Hue keeps as an exact replay. The losses are still counted and
838
+ * reported. */
839
+ omitDroppedRecords() {
840
+ this.marksDroppedRecords = false;
841
+ }
832
842
  /** @internal Exporter callback: these counts reached Hue on their traces' roots. Only what a
833
843
  * root carried is forgotten: a record lost while its request was in flight is still counted. */
834
844
  consumeDroppedRecords(counts) {
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Package version shared by the instrumentation scope and the export User-Agent. */
2
- export declare const sdkVersion = "0.18.0";
2
+ export declare const sdkVersion = "0.18.1";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/write-version.mjs from package.json; do not edit by hand.
2
2
  /** Package version shared by the instrumentation scope and the export User-Agent. */
3
- export const sdkVersion = "0.18.0";
3
+ export const sdkVersion = "0.18.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hue-run/sdk",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "publishConfig": {