deepline 0.3.234 → 0.3.236

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.
@@ -204,6 +204,18 @@ function describeRequestTarget(path: string): string {
204
204
  return 'Deepline';
205
205
  }
206
206
 
207
+ function networkDiagnosticUrl(value: string): string | undefined {
208
+ try {
209
+ const url = new URL(value);
210
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') return undefined;
211
+ // Diagnostics never retain URL credentials, query values, or fragments.
212
+ return `${url.origin}${url.pathname}`;
213
+ } catch {
214
+ // Preserve the original transport failure for malformed request URLs.
215
+ return undefined;
216
+ }
217
+ }
218
+
207
219
  function providerLabelFromToolId(toolId: string): string {
208
220
  const normalized = toolId.trim().toLowerCase();
209
221
  const provider = normalized.split(/[_:./-]+/)[0];
@@ -379,6 +391,10 @@ export class HttpClient {
379
391
  const baseUrl = this.config.baseUrl;
380
392
  const url = `${baseUrl}${path}`;
381
393
  const method = options?.method ?? 'GET';
394
+ const requestStartedAt = Date.now();
395
+ let fetchAttempts = 0;
396
+ let lastRequestUrl = url;
397
+ let networkFailedAt: number | undefined;
382
398
 
383
399
  const headers = this.authHeaders(options?.headers);
384
400
 
@@ -397,6 +413,7 @@ export class HttpClient {
397
413
  requestAttempts: for (let attempt = 0; attempt <= maxRetries; attempt++) {
398
414
  if (options?.signal?.aborted) {
399
415
  lastError = requestCancelledError(options.signal);
416
+ networkFailedAt = Date.now();
400
417
  break;
401
418
  }
402
419
  if (attempt > 0) {
@@ -410,6 +427,7 @@ export class HttpClient {
410
427
  await sleep(delayMs, options?.signal);
411
428
  } catch (error) {
412
429
  lastError = error instanceof Error ? error : new Error(String(error));
430
+ networkFailedAt = Date.now();
413
431
  break;
414
432
  }
415
433
  }
@@ -417,6 +435,7 @@ export class HttpClient {
417
435
  for (const candidateUrl of candidateUrls) {
418
436
  if (options?.signal?.aborted) {
419
437
  lastError = requestCancelledError(options.signal);
438
+ networkFailedAt = Date.now();
420
439
  break requestAttempts;
421
440
  }
422
441
  const controller = new AbortController();
@@ -432,17 +451,20 @@ export class HttpClient {
432
451
  });
433
452
 
434
453
  try {
454
+ const requestBody =
455
+ options?.formData !== undefined
456
+ ? typeof options.formData === 'function'
457
+ ? options.formData()
458
+ : options.formData
459
+ : options?.body !== undefined
460
+ ? JSON.stringify(options.body)
461
+ : undefined;
462
+ fetchAttempts += 1;
463
+ lastRequestUrl = candidateUrl;
435
464
  const response = await fetch(candidateUrl, {
436
465
  method,
437
466
  headers,
438
- body:
439
- options?.formData !== undefined
440
- ? typeof options.formData === 'function'
441
- ? options.formData()
442
- : options.formData
443
- : options?.body !== undefined
444
- ? JSON.stringify(options.body)
445
- : undefined,
467
+ body: requestBody,
446
468
  signal: controller.signal,
447
469
  });
448
470
 
@@ -605,6 +627,7 @@ export class HttpClient {
605
627
  return parsed as T;
606
628
  } catch (error) {
607
629
  clearTimeout(timeoutId);
630
+ networkFailedAt = Date.now();
608
631
  if (options?.signal?.aborted) {
609
632
  lastError = requestCancelledError(options.signal);
610
633
  break requestAttempts;
@@ -637,6 +660,19 @@ export class HttpClient {
637
660
  lastError,
638
661
  describeRequestTarget(path),
639
662
  );
663
+ const failedAt = networkFailedAt ?? Date.now();
664
+ const diagnosticUrl = networkDiagnosticUrl(lastRequestUrl);
665
+ const networkDetails = {
666
+ ...mappedNetworkError.details,
667
+ request: {
668
+ method,
669
+ ...(diagnosticUrl === undefined ? {} : { url: diagnosticUrl }),
670
+ failedAtUtc: new Date(failedAt).toISOString(),
671
+ // Elapsed time covers the entire logical request, including retries.
672
+ elapsedMs: Math.max(0, failedAt - requestStartedAt),
673
+ attempts: fetchAttempts,
674
+ },
675
+ };
640
676
  if (options?.toolId) {
641
677
  const code = mappedNetworkError.code;
642
678
  throw withErrorCause(
@@ -662,7 +698,7 @@ export class HttpClient {
662
698
  ? 'unknown'
663
699
  : 'unavailable',
664
700
  networkScope: 'client_to_deepline',
665
- details: mappedNetworkError.details,
701
+ details: networkDetails,
666
702
  publicDetails: options.idempotencyKey
667
703
  ? { idempotencyKey: options.idempotencyKey }
668
704
  : null,
@@ -676,7 +712,7 @@ export class HttpClient {
676
712
  withCoworkNetworkHint(mappedNetworkError.message),
677
713
  undefined,
678
714
  mappedNetworkError.code,
679
- mappedNetworkError.details,
715
+ networkDetails,
680
716
  ),
681
717
  lastError,
682
718
  );
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.234',
203
+ version: '0.3.236',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -43,6 +43,7 @@ export type PlayRunRecoveryReason =
43
43
  | 'modal_runner_start_failed_before_execution'
44
44
  | 'daytona_sandbox_transition_pending_before_execution'
45
45
  | 'daytona_sandbox_acquisition_unavailable_before_execution'
46
+ | 'runtime_scheduler_unavailable_before_execution'
46
47
  | 'runtime_initialization_transport_before_execution'
47
48
  | 'runtime_traffic_hold_before_execution'
48
49
  | 'runtime_traffic_throttled_before_execution';
@@ -211,6 +212,27 @@ export function recoveryForRuntimeTrafficPolicy(input: {
211
212
  });
212
213
  }
213
214
 
215
+ /**
216
+ * Scheduler admission must be durable before a provider create. If that
217
+ * control-plane read/write is temporarily unavailable, defer through the
218
+ * recovery ledger instead of turning an unstarted Play into a runner failure.
219
+ */
220
+ export function recoveryForRuntimeSchedulerAdmissionFailure(
221
+ error: unknown,
222
+ ): PlayRunRecoveryError {
223
+ return new PlayRunRecoveryError({
224
+ decision: {
225
+ stage: 'pre_code',
226
+ action: 'defer',
227
+ reason: 'runtime_scheduler_unavailable_before_execution',
228
+ dependency: 'runtime',
229
+ },
230
+ message:
231
+ 'Runtime scheduler admission is temporarily unavailable before customer execution began.',
232
+ cause: error,
233
+ });
234
+ }
235
+
214
236
  /**
215
237
  * Daytona acknowledged a named sandbox but has not finished creating it.
216
238
  * Resume that durable provider operation after a bounded defer; another
@@ -38,7 +38,10 @@ import {
38
38
  type OneShotDaytonaSandboxLifecycle,
39
39
  validateDaytonaExecutionContext,
40
40
  } from './daytona-lifecycle';
41
- import { recoveryForDaytonaSandboxTransitionPending } from '../../play-run-recovery-policy';
41
+ import {
42
+ isPlayRunRecoveryError,
43
+ recoveryForDaytonaSandboxTransitionPending,
44
+ } from '../../play-run-recovery-policy';
42
45
  import { stageDaytonaRunnerPayload } from './daytona-payload-transport';
43
46
  import {
44
47
  DaytonaRunnerInitializationError,
@@ -1819,6 +1822,9 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
1819
1822
  if (error instanceof DaytonaSandboxAcquisitionUnavailableError) {
1820
1823
  throw error;
1821
1824
  }
1825
+ if (isPlayRunRecoveryError(error)) {
1826
+ throw error;
1827
+ }
1822
1828
  emitDaytonaStage(callbacks, config.context, 'execute:error', {
1823
1829
  sandboxId: sandboxCleanup.currentSandbox()?.id ?? null,
1824
1830
  runnerAttempt: Math.max(0, Math.floor(config.context.runAttempt ?? 0)),
@@ -11,6 +11,7 @@
11
11
  * cross-call session state.
12
12
  */
13
13
  import { createRequire } from 'node:module';
14
+ import { AsyncLocalStorage } from 'node:async_hooks';
14
15
  import { readFileSync } from 'node:fs';
15
16
  import path from 'node:path';
16
17
  import {
@@ -30,6 +31,18 @@ import {
30
31
 
31
32
  export { ABSURD_SCHEMA_VERSION };
32
33
 
34
+ const absurdSchedulerQuerySignal = new AsyncLocalStorage<AbortSignal>();
35
+
36
+ /** Applies cancellation to this async operation's Absurd SQL calls only.
37
+ * The SDK has no signal argument; keep cancellation in its Deepline adapter,
38
+ * without affecting other tasks sharing the same queryable or pool. */
39
+ export function runWithAbsurdSchedulerQuerySignal<T>(
40
+ signal: AbortSignal,
41
+ operation: () => Promise<T>,
42
+ ): Promise<T> {
43
+ return absurdSchedulerQuerySignal.run(signal, operation);
44
+ }
45
+
33
46
  /**
34
47
  * Builds an SDK `Queryable` backed by the hardened runtime scheduler pool.
35
48
  * Every query runs on a freshly checked-out pooled client and releases it,
@@ -43,13 +56,19 @@ export function absurdQueryableForRuntimeScheduler(
43
56
  sql: string,
44
57
  params?: unknown[],
45
58
  ): Promise<QueryResult<Row>> => {
46
- return withRuntimeSchedulerClient(poolOptions, async (client) => {
47
- const result = await client.query<Row & Record<string, unknown>>(
48
- sql,
49
- params,
50
- );
51
- return { rows: result.rows as Row[] };
52
- });
59
+ const signal = absurdSchedulerQuerySignal.getStore();
60
+ return withRuntimeSchedulerClient(
61
+ poolOptions,
62
+ async (client) => {
63
+ const result = await client.query<Row & Record<string, unknown>>(
64
+ sql,
65
+ params,
66
+ );
67
+ return { rows: result.rows as Row[] };
68
+ },
69
+ undefined,
70
+ { signal },
71
+ );
53
72
  },
54
73
  };
55
74
  }
@@ -50,6 +50,7 @@ import { runtimeLimitExceededCause } from '../run-failure';
50
50
  import {
51
51
  recoveryForDaytonaSandboxAcquisitionUnavailable,
52
52
  recoveryForRuntimeInitializationTransportFailure,
53
+ recoveryForRuntimeSchedulerAdmissionFailure,
53
54
  recoveryForRuntimeTrafficPolicy,
54
55
  } from '../play-run-recovery-policy';
55
56
  import type { PlayCheckpoint } from '../ctx-types';
@@ -208,9 +209,10 @@ import {
208
209
  } from './absurd-worker-required-env';
209
210
  import {
210
211
  acquirePgNotifyHub,
212
+ isConnectionCategoryError,
211
213
  isRuntimeSchedulerCapacityError,
214
+ isRuntimeSchedulerQueryCancellationError,
212
215
  isRuntimeSchedulerRetryableTransactionError,
213
- isConnectionCategoryError,
214
216
  parseRuntimeSchedulerCapacityError,
215
217
  RuntimeSchedulerCapacityError,
216
218
  runtimeSchedulerCapacityRetryAfterMs,
@@ -219,6 +221,7 @@ import {
219
221
  type RuntimeSchedulerPoolOptions,
220
222
  withRuntimeSchedulerClient,
221
223
  } from './postgres-pool';
224
+ import { runWithAbsurdSchedulerQuerySignal } from './absurd-pool';
222
225
  import {
223
226
  createPostgresSchedulerProgressReporter,
224
227
  type PostgresSchedulerProgressReporter,
@@ -548,7 +551,10 @@ export async function runWithAbsurdExecutionLeaseHeartbeat<T>(input: {
548
551
  lastHeartbeatAt = now;
549
552
  heartbeatState.inFlight = (async () => {
550
553
  try {
551
- await input.ctx.heartbeat();
554
+ await runWithAbsurdSchedulerQuerySignal(
555
+ cancellationController.signal,
556
+ () => input.ctx.heartbeat(),
557
+ );
552
558
  } catch (error) {
553
559
  if (!isTransientHeartbeatTimeout(error) || stopped) throw error;
554
560
  console.warn('[absurd-runtime.worker] lease_heartbeat_timeout_retry', {
@@ -557,10 +563,22 @@ export async function runWithAbsurdExecutionLeaseHeartbeat<T>(input: {
557
563
  });
558
564
  await sleep(heartbeatTimeoutRetryDelayMs);
559
565
  if (stopped) return;
560
- await input.ctx.heartbeat();
566
+ await runWithAbsurdSchedulerQuerySignal(
567
+ cancellationController.signal,
568
+ () => input.ctx.heartbeat(),
569
+ );
561
570
  }
562
571
  })()
563
572
  .catch((error) => {
573
+ // The handoff deadline owns this cancellation. A cancelled local SQL
574
+ // call does not prove that the engine cancelled the durable task.
575
+ if (
576
+ cancellationController.signal.aborted &&
577
+ error === cancellationController.signal.reason &&
578
+ error instanceof AbsurdExecutionOperationTimeoutError
579
+ ) {
580
+ return;
581
+ }
564
582
  const engineTerminal =
565
583
  error instanceof CancelledTask || error instanceof FailedTask;
566
584
  if (engineTerminal) {
@@ -593,9 +611,22 @@ export async function runWithAbsurdExecutionLeaseHeartbeat<T>(input: {
593
611
  timer.unref?.();
594
612
  let operationTimeout: ReturnType<typeof setTimeout> | null = null;
595
613
  try {
596
- const operation = Promise.resolve().then(() =>
597
- input.operation(cancellationController.signal),
598
- );
614
+ // A completed backend still cannot park until its in-flight lease update
615
+ // settles. Keep that drain inside the same supervised handoff deadline.
616
+ const operation = (async () => {
617
+ try {
618
+ operationResult = await Promise.resolve().then(() =>
619
+ input.operation(cancellationController.signal),
620
+ );
621
+ } catch (error) {
622
+ operationError = error;
623
+ } finally {
624
+ stopped = true;
625
+ stopHeartbeatTimer();
626
+ await heartbeatState.inFlight;
627
+ }
628
+ return operationResult;
629
+ })();
599
630
  if (operationTimeoutMs !== null) {
600
631
  operationResult = await Promise.race([
601
632
  operation,
@@ -617,15 +648,11 @@ export async function runWithAbsurdExecutionLeaseHeartbeat<T>(input: {
617
648
  operationResult = await operation;
618
649
  }
619
650
  } catch (error) {
620
- operationError = error;
651
+ operationError ??= error;
621
652
  } finally {
622
653
  if (operationTimeout) clearTimeout(operationTimeout);
623
654
  stopped = true;
624
655
  stopHeartbeatTimer();
625
- const pendingHeartbeat = heartbeatState.inFlight;
626
- if (pendingHeartbeat) {
627
- await pendingHeartbeat.catch(() => undefined);
628
- }
629
656
  }
630
657
  if (heartbeatFailure) {
631
658
  if (
@@ -3490,22 +3517,85 @@ async function executeAssignedAbsurdAttempt(input: {
3490
3517
  );
3491
3518
  }
3492
3519
  const mode = runtimeSandboxCapacityAdmissionMode();
3493
- const reserved = await deps.runWithClient((client) =>
3494
- reserveRuntimeSandboxOperation(
3495
- client,
3520
+ const reservationInput = {
3521
+ orgId: launch.orgId,
3522
+ runId,
3523
+ provider,
3524
+ mode,
3525
+ attempt: generation,
3526
+ environment: 'production' as const,
3527
+ resumePendingDaytonaOperation: provider === 'daytona',
3528
+ operationKey: `sandbox-operation:${runId}:${generation}:${provider}:${providerAttempt ?? 1}`,
3529
+ };
3530
+ const reserve = () =>
3531
+ deps.runWithClient((client) =>
3532
+ reserveRuntimeSandboxOperation(
3533
+ client,
3534
+ reservationInput,
3535
+ options,
3536
+ ),
3537
+ );
3538
+ const deferForSchedulerFailure = (error: unknown): never => {
3539
+ console.warn(
3540
+ '[absurd-runtime.worker] sandbox_capacity_admission_deferred',
3496
3541
  {
3497
- orgId: launch.orgId,
3498
3542
  runId,
3499
3543
  provider,
3500
- mode,
3501
- attempt: generation,
3502
- environment: 'production',
3503
- resumePendingDaytonaOperation: provider === 'daytona',
3504
- operationKey: `sandbox-operation:${runId}:${generation}:${provider}:${providerAttempt ?? 1}`,
3544
+ providerAttempt: providerAttempt ?? 1,
3545
+ error:
3546
+ error instanceof Error
3547
+ ? error.message
3548
+ : String(error),
3505
3549
  },
3506
- options,
3507
- ),
3508
- );
3550
+ );
3551
+ throw recoveryForRuntimeSchedulerAdmissionFailure(error);
3552
+ };
3553
+ const isRetryableSchedulerFailure = (error: unknown) =>
3554
+ isRuntimeSchedulerCapacityError(error) ||
3555
+ isConnectionCategoryError(error) ||
3556
+ isRuntimeSchedulerRetryableTransactionError(error) ||
3557
+ isRuntimeSchedulerQueryCancellationError(error);
3558
+ let reserved: Awaited<ReturnType<typeof reserve>> | null =
3559
+ null;
3560
+ try {
3561
+ reserved = await reserve();
3562
+ } catch (error) {
3563
+ if (isRuntimeSchedulerQueryCancellationError(error)) {
3564
+ // A query timeout has an ambiguous outcome. The stable
3565
+ // operation key makes this reservation safe to repeat:
3566
+ // a committed first attempt is read back, and a missing
3567
+ // one is inserted once.
3568
+ console.warn(
3569
+ '[absurd-runtime.worker] sandbox_capacity_admission_retry',
3570
+ {
3571
+ runId,
3572
+ provider,
3573
+ providerAttempt: providerAttempt ?? 1,
3574
+ error:
3575
+ error instanceof Error
3576
+ ? error.message
3577
+ : String(error),
3578
+ },
3579
+ );
3580
+ try {
3581
+ reserved = await reserve();
3582
+ } catch (retryError) {
3583
+ if (!isRetryableSchedulerFailure(retryError)) {
3584
+ throw retryError;
3585
+ }
3586
+ deferForSchedulerFailure(retryError);
3587
+ }
3588
+ } else if (isRetryableSchedulerFailure(error)) {
3589
+ deferForSchedulerFailure(error);
3590
+ } else {
3591
+ throw error;
3592
+ }
3593
+ }
3594
+ if (reserved === null) {
3595
+ throw new Error(
3596
+ 'Runtime scheduler sandbox capacity reservation returned no result.',
3597
+ );
3598
+ }
3509
3599
  if (!reserved.granted) {
3510
3600
  throw new RuntimeSandboxCapacityLimitError({
3511
3601
  orgId: launch.orgId,
package/dist/cli/index.js CHANGED
@@ -3454,7 +3454,7 @@ var SDK_RELEASE = {
3454
3454
  // getters keep their established compatibility behavior.
3455
3455
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
3456
3456
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
3457
- version: "0.3.234",
3457
+ version: "0.3.236",
3458
3458
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
3459
3459
  packageCapabilities: {
3460
3460
  updatePreferences: 1
@@ -4133,6 +4133,15 @@ function describeRequestTarget(path) {
4133
4133
  }
4134
4134
  return "Deepline";
4135
4135
  }
4136
+ function networkDiagnosticUrl(value) {
4137
+ try {
4138
+ const url = new URL(value);
4139
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
4140
+ return `${url.origin}${url.pathname}`;
4141
+ } catch {
4142
+ return void 0;
4143
+ }
4144
+ }
4136
4145
  function providerLabelFromToolId(toolId) {
4137
4146
  const normalized = toolId.trim().toLowerCase();
4138
4147
  const provider = normalized.split(/[_:./-]+/)[0];
@@ -4233,6 +4242,10 @@ var HttpClient = class {
4233
4242
  const baseUrl = this.config.baseUrl;
4234
4243
  const url = `${baseUrl}${path}`;
4235
4244
  const method = options?.method ?? "GET";
4245
+ const requestStartedAt = Date.now();
4246
+ let fetchAttempts = 0;
4247
+ let lastRequestUrl = url;
4248
+ let networkFailedAt;
4236
4249
  const headers = this.authHeaders(options?.headers);
4237
4250
  if (options?.body !== void 0) {
4238
4251
  headers["Content-Type"] = "application/json";
@@ -4244,6 +4257,7 @@ var HttpClient = class {
4244
4257
  requestAttempts: for (let attempt = 0; attempt <= maxRetries; attempt++) {
4245
4258
  if (options?.signal?.aborted) {
4246
4259
  lastError = requestCancelledError(options.signal);
4260
+ networkFailedAt = Date.now();
4247
4261
  break;
4248
4262
  }
4249
4263
  if (attempt > 0) {
@@ -4254,12 +4268,14 @@ var HttpClient = class {
4254
4268
  await sleep(delayMs, options?.signal);
4255
4269
  } catch (error) {
4256
4270
  lastError = error instanceof Error ? error : new Error(String(error));
4271
+ networkFailedAt = Date.now();
4257
4272
  break;
4258
4273
  }
4259
4274
  }
4260
4275
  for (const candidateUrl of candidateUrls) {
4261
4276
  if (options?.signal?.aborted) {
4262
4277
  lastError = requestCancelledError(options.signal);
4278
+ networkFailedAt = Date.now();
4263
4279
  break requestAttempts;
4264
4280
  }
4265
4281
  const controller = new AbortController();
@@ -4274,10 +4290,13 @@ var HttpClient = class {
4274
4290
  once: true
4275
4291
  });
4276
4292
  try {
4293
+ const requestBody = options?.formData !== void 0 ? typeof options.formData === "function" ? options.formData() : options.formData : options?.body !== void 0 ? JSON.stringify(options.body) : void 0;
4294
+ fetchAttempts += 1;
4295
+ lastRequestUrl = candidateUrl;
4277
4296
  const response = await fetch(candidateUrl, {
4278
4297
  method,
4279
4298
  headers,
4280
- body: options?.formData !== void 0 ? typeof options.formData === "function" ? options.formData() : options.formData : options?.body !== void 0 ? JSON.stringify(options.body) : void 0,
4299
+ body: requestBody,
4281
4300
  signal: controller.signal
4282
4301
  });
4283
4302
  options?.onResponse?.(response);
@@ -4381,6 +4400,7 @@ var HttpClient = class {
4381
4400
  return parsed;
4382
4401
  } catch (error) {
4383
4402
  clearTimeout(timeoutId);
4403
+ networkFailedAt = Date.now();
4384
4404
  if (options?.signal?.aborted) {
4385
4405
  lastError = requestCancelledError(options.signal);
4386
4406
  break requestAttempts;
@@ -4411,6 +4431,19 @@ var HttpClient = class {
4411
4431
  lastError,
4412
4432
  describeRequestTarget(path)
4413
4433
  );
4434
+ const failedAt = networkFailedAt ?? Date.now();
4435
+ const diagnosticUrl = networkDiagnosticUrl(lastRequestUrl);
4436
+ const networkDetails = {
4437
+ ...mappedNetworkError.details,
4438
+ request: {
4439
+ method,
4440
+ ...diagnosticUrl === void 0 ? {} : { url: diagnosticUrl },
4441
+ failedAtUtc: new Date(failedAt).toISOString(),
4442
+ // Elapsed time covers the entire logical request, including retries.
4443
+ elapsedMs: Math.max(0, failedAt - requestStartedAt),
4444
+ attempts: fetchAttempts
4445
+ }
4446
+ };
4414
4447
  if (options?.toolId) {
4415
4448
  const code = mappedNetworkError.code;
4416
4449
  throw withErrorCause(
@@ -4431,7 +4464,7 @@ var HttpClient = class {
4431
4464
  retryAfterMs: null,
4432
4465
  networkKind: code === "NETWORK_TIMEOUT" ? "timeout" : code === "NETWORK_ABORTED" ? "unknown" : "unavailable",
4433
4466
  networkScope: "client_to_deepline",
4434
- details: mappedNetworkError.details,
4467
+ details: networkDetails,
4435
4468
  publicDetails: options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : null
4436
4469
  }
4437
4470
  ),
@@ -4443,7 +4476,7 @@ var HttpClient = class {
4443
4476
  withCoworkNetworkHint(mappedNetworkError.message),
4444
4477
  void 0,
4445
4478
  mappedNetworkError.code,
4446
- mappedNetworkError.details
4479
+ networkDetails
4447
4480
  ),
4448
4481
  lastError
4449
4482
  );
@@ -3449,7 +3449,7 @@ var SDK_RELEASE = {
3449
3449
  // getters keep their established compatibility behavior.
3450
3450
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
3451
3451
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
3452
- version: "0.3.234",
3452
+ version: "0.3.236",
3453
3453
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
3454
3454
  packageCapabilities: {
3455
3455
  updatePreferences: 1
@@ -4128,6 +4128,15 @@ function describeRequestTarget(path) {
4128
4128
  }
4129
4129
  return "Deepline";
4130
4130
  }
4131
+ function networkDiagnosticUrl(value) {
4132
+ try {
4133
+ const url = new URL(value);
4134
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
4135
+ return `${url.origin}${url.pathname}`;
4136
+ } catch {
4137
+ return void 0;
4138
+ }
4139
+ }
4131
4140
  function providerLabelFromToolId(toolId) {
4132
4141
  const normalized = toolId.trim().toLowerCase();
4133
4142
  const provider = normalized.split(/[_:./-]+/)[0];
@@ -4228,6 +4237,10 @@ var HttpClient = class {
4228
4237
  const baseUrl = this.config.baseUrl;
4229
4238
  const url = `${baseUrl}${path}`;
4230
4239
  const method = options?.method ?? "GET";
4240
+ const requestStartedAt = Date.now();
4241
+ let fetchAttempts = 0;
4242
+ let lastRequestUrl = url;
4243
+ let networkFailedAt;
4231
4244
  const headers = this.authHeaders(options?.headers);
4232
4245
  if (options?.body !== void 0) {
4233
4246
  headers["Content-Type"] = "application/json";
@@ -4239,6 +4252,7 @@ var HttpClient = class {
4239
4252
  requestAttempts: for (let attempt = 0; attempt <= maxRetries; attempt++) {
4240
4253
  if (options?.signal?.aborted) {
4241
4254
  lastError = requestCancelledError(options.signal);
4255
+ networkFailedAt = Date.now();
4242
4256
  break;
4243
4257
  }
4244
4258
  if (attempt > 0) {
@@ -4249,12 +4263,14 @@ var HttpClient = class {
4249
4263
  await sleep(delayMs, options?.signal);
4250
4264
  } catch (error) {
4251
4265
  lastError = error instanceof Error ? error : new Error(String(error));
4266
+ networkFailedAt = Date.now();
4252
4267
  break;
4253
4268
  }
4254
4269
  }
4255
4270
  for (const candidateUrl of candidateUrls) {
4256
4271
  if (options?.signal?.aborted) {
4257
4272
  lastError = requestCancelledError(options.signal);
4273
+ networkFailedAt = Date.now();
4258
4274
  break requestAttempts;
4259
4275
  }
4260
4276
  const controller = new AbortController();
@@ -4269,10 +4285,13 @@ var HttpClient = class {
4269
4285
  once: true
4270
4286
  });
4271
4287
  try {
4288
+ const requestBody = options?.formData !== void 0 ? typeof options.formData === "function" ? options.formData() : options.formData : options?.body !== void 0 ? JSON.stringify(options.body) : void 0;
4289
+ fetchAttempts += 1;
4290
+ lastRequestUrl = candidateUrl;
4272
4291
  const response = await fetch(candidateUrl, {
4273
4292
  method,
4274
4293
  headers,
4275
- body: options?.formData !== void 0 ? typeof options.formData === "function" ? options.formData() : options.formData : options?.body !== void 0 ? JSON.stringify(options.body) : void 0,
4294
+ body: requestBody,
4276
4295
  signal: controller.signal
4277
4296
  });
4278
4297
  options?.onResponse?.(response);
@@ -4376,6 +4395,7 @@ var HttpClient = class {
4376
4395
  return parsed;
4377
4396
  } catch (error) {
4378
4397
  clearTimeout(timeoutId);
4398
+ networkFailedAt = Date.now();
4379
4399
  if (options?.signal?.aborted) {
4380
4400
  lastError = requestCancelledError(options.signal);
4381
4401
  break requestAttempts;
@@ -4406,6 +4426,19 @@ var HttpClient = class {
4406
4426
  lastError,
4407
4427
  describeRequestTarget(path)
4408
4428
  );
4429
+ const failedAt = networkFailedAt ?? Date.now();
4430
+ const diagnosticUrl = networkDiagnosticUrl(lastRequestUrl);
4431
+ const networkDetails = {
4432
+ ...mappedNetworkError.details,
4433
+ request: {
4434
+ method,
4435
+ ...diagnosticUrl === void 0 ? {} : { url: diagnosticUrl },
4436
+ failedAtUtc: new Date(failedAt).toISOString(),
4437
+ // Elapsed time covers the entire logical request, including retries.
4438
+ elapsedMs: Math.max(0, failedAt - requestStartedAt),
4439
+ attempts: fetchAttempts
4440
+ }
4441
+ };
4409
4442
  if (options?.toolId) {
4410
4443
  const code = mappedNetworkError.code;
4411
4444
  throw withErrorCause(
@@ -4426,7 +4459,7 @@ var HttpClient = class {
4426
4459
  retryAfterMs: null,
4427
4460
  networkKind: code === "NETWORK_TIMEOUT" ? "timeout" : code === "NETWORK_ABORTED" ? "unknown" : "unavailable",
4428
4461
  networkScope: "client_to_deepline",
4429
- details: mappedNetworkError.details,
4462
+ details: networkDetails,
4430
4463
  publicDetails: options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : null
4431
4464
  }
4432
4465
  ),
@@ -4438,7 +4471,7 @@ var HttpClient = class {
4438
4471
  withCoworkNetworkHint(mappedNetworkError.message),
4439
4472
  void 0,
4440
4473
  mappedNetworkError.code,
4441
- mappedNetworkError.details
4474
+ networkDetails
4442
4475
  ),
4443
4476
  lastError
4444
4477
  );
package/dist/index.js CHANGED
@@ -1267,7 +1267,7 @@ var SDK_RELEASE = {
1267
1267
  // getters keep their established compatibility behavior.
1268
1268
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
1269
1269
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
1270
- version: "0.3.234",
1270
+ version: "0.3.236",
1271
1271
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
1272
1272
  packageCapabilities: {
1273
1273
  updatePreferences: 1
@@ -1909,6 +1909,15 @@ function describeRequestTarget(path) {
1909
1909
  }
1910
1910
  return "Deepline";
1911
1911
  }
1912
+ function networkDiagnosticUrl(value) {
1913
+ try {
1914
+ const url = new URL(value);
1915
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
1916
+ return `${url.origin}${url.pathname}`;
1917
+ } catch {
1918
+ return void 0;
1919
+ }
1920
+ }
1912
1921
  function providerLabelFromToolId(toolId) {
1913
1922
  const normalized = toolId.trim().toLowerCase();
1914
1923
  const provider = normalized.split(/[_:./-]+/)[0];
@@ -2009,6 +2018,10 @@ var HttpClient = class {
2009
2018
  const baseUrl = this.config.baseUrl;
2010
2019
  const url = `${baseUrl}${path}`;
2011
2020
  const method = options?.method ?? "GET";
2021
+ const requestStartedAt = Date.now();
2022
+ let fetchAttempts = 0;
2023
+ let lastRequestUrl = url;
2024
+ let networkFailedAt;
2012
2025
  const headers = this.authHeaders(options?.headers);
2013
2026
  if (options?.body !== void 0) {
2014
2027
  headers["Content-Type"] = "application/json";
@@ -2020,6 +2033,7 @@ var HttpClient = class {
2020
2033
  requestAttempts: for (let attempt = 0; attempt <= maxRetries; attempt++) {
2021
2034
  if (options?.signal?.aborted) {
2022
2035
  lastError = requestCancelledError(options.signal);
2036
+ networkFailedAt = Date.now();
2023
2037
  break;
2024
2038
  }
2025
2039
  if (attempt > 0) {
@@ -2030,12 +2044,14 @@ var HttpClient = class {
2030
2044
  await sleep(delayMs, options?.signal);
2031
2045
  } catch (error) {
2032
2046
  lastError = error instanceof Error ? error : new Error(String(error));
2047
+ networkFailedAt = Date.now();
2033
2048
  break;
2034
2049
  }
2035
2050
  }
2036
2051
  for (const candidateUrl of candidateUrls) {
2037
2052
  if (options?.signal?.aborted) {
2038
2053
  lastError = requestCancelledError(options.signal);
2054
+ networkFailedAt = Date.now();
2039
2055
  break requestAttempts;
2040
2056
  }
2041
2057
  const controller = new AbortController();
@@ -2050,10 +2066,13 @@ var HttpClient = class {
2050
2066
  once: true
2051
2067
  });
2052
2068
  try {
2069
+ const requestBody = options?.formData !== void 0 ? typeof options.formData === "function" ? options.formData() : options.formData : options?.body !== void 0 ? JSON.stringify(options.body) : void 0;
2070
+ fetchAttempts += 1;
2071
+ lastRequestUrl = candidateUrl;
2053
2072
  const response = await fetch(candidateUrl, {
2054
2073
  method,
2055
2074
  headers,
2056
- body: options?.formData !== void 0 ? typeof options.formData === "function" ? options.formData() : options.formData : options?.body !== void 0 ? JSON.stringify(options.body) : void 0,
2075
+ body: requestBody,
2057
2076
  signal: controller.signal
2058
2077
  });
2059
2078
  options?.onResponse?.(response);
@@ -2157,6 +2176,7 @@ var HttpClient = class {
2157
2176
  return parsed;
2158
2177
  } catch (error) {
2159
2178
  clearTimeout(timeoutId);
2179
+ networkFailedAt = Date.now();
2160
2180
  if (options?.signal?.aborted) {
2161
2181
  lastError = requestCancelledError(options.signal);
2162
2182
  break requestAttempts;
@@ -2187,6 +2207,19 @@ var HttpClient = class {
2187
2207
  lastError,
2188
2208
  describeRequestTarget(path)
2189
2209
  );
2210
+ const failedAt = networkFailedAt ?? Date.now();
2211
+ const diagnosticUrl = networkDiagnosticUrl(lastRequestUrl);
2212
+ const networkDetails = {
2213
+ ...mappedNetworkError.details,
2214
+ request: {
2215
+ method,
2216
+ ...diagnosticUrl === void 0 ? {} : { url: diagnosticUrl },
2217
+ failedAtUtc: new Date(failedAt).toISOString(),
2218
+ // Elapsed time covers the entire logical request, including retries.
2219
+ elapsedMs: Math.max(0, failedAt - requestStartedAt),
2220
+ attempts: fetchAttempts
2221
+ }
2222
+ };
2190
2223
  if (options?.toolId) {
2191
2224
  const code = mappedNetworkError.code;
2192
2225
  throw withErrorCause(
@@ -2207,7 +2240,7 @@ var HttpClient = class {
2207
2240
  retryAfterMs: null,
2208
2241
  networkKind: code === "NETWORK_TIMEOUT" ? "timeout" : code === "NETWORK_ABORTED" ? "unknown" : "unavailable",
2209
2242
  networkScope: "client_to_deepline",
2210
- details: mappedNetworkError.details,
2243
+ details: networkDetails,
2211
2244
  publicDetails: options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : null
2212
2245
  }
2213
2246
  ),
@@ -2219,7 +2252,7 @@ var HttpClient = class {
2219
2252
  withCoworkNetworkHint(mappedNetworkError.message),
2220
2253
  void 0,
2221
2254
  mappedNetworkError.code,
2222
- mappedNetworkError.details
2255
+ networkDetails
2223
2256
  ),
2224
2257
  lastError
2225
2258
  );
package/dist/index.mjs CHANGED
@@ -1171,7 +1171,7 @@ var SDK_RELEASE = {
1171
1171
  // getters keep their established compatibility behavior.
1172
1172
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
1173
1173
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
1174
- version: "0.3.234",
1174
+ version: "0.3.236",
1175
1175
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
1176
1176
  packageCapabilities: {
1177
1177
  updatePreferences: 1
@@ -1813,6 +1813,15 @@ function describeRequestTarget(path) {
1813
1813
  }
1814
1814
  return "Deepline";
1815
1815
  }
1816
+ function networkDiagnosticUrl(value) {
1817
+ try {
1818
+ const url = new URL(value);
1819
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
1820
+ return `${url.origin}${url.pathname}`;
1821
+ } catch {
1822
+ return void 0;
1823
+ }
1824
+ }
1816
1825
  function providerLabelFromToolId(toolId) {
1817
1826
  const normalized = toolId.trim().toLowerCase();
1818
1827
  const provider = normalized.split(/[_:./-]+/)[0];
@@ -1913,6 +1922,10 @@ var HttpClient = class {
1913
1922
  const baseUrl = this.config.baseUrl;
1914
1923
  const url = `${baseUrl}${path}`;
1915
1924
  const method = options?.method ?? "GET";
1925
+ const requestStartedAt = Date.now();
1926
+ let fetchAttempts = 0;
1927
+ let lastRequestUrl = url;
1928
+ let networkFailedAt;
1916
1929
  const headers = this.authHeaders(options?.headers);
1917
1930
  if (options?.body !== void 0) {
1918
1931
  headers["Content-Type"] = "application/json";
@@ -1924,6 +1937,7 @@ var HttpClient = class {
1924
1937
  requestAttempts: for (let attempt = 0; attempt <= maxRetries; attempt++) {
1925
1938
  if (options?.signal?.aborted) {
1926
1939
  lastError = requestCancelledError(options.signal);
1940
+ networkFailedAt = Date.now();
1927
1941
  break;
1928
1942
  }
1929
1943
  if (attempt > 0) {
@@ -1934,12 +1948,14 @@ var HttpClient = class {
1934
1948
  await sleep(delayMs, options?.signal);
1935
1949
  } catch (error) {
1936
1950
  lastError = error instanceof Error ? error : new Error(String(error));
1951
+ networkFailedAt = Date.now();
1937
1952
  break;
1938
1953
  }
1939
1954
  }
1940
1955
  for (const candidateUrl of candidateUrls) {
1941
1956
  if (options?.signal?.aborted) {
1942
1957
  lastError = requestCancelledError(options.signal);
1958
+ networkFailedAt = Date.now();
1943
1959
  break requestAttempts;
1944
1960
  }
1945
1961
  const controller = new AbortController();
@@ -1954,10 +1970,13 @@ var HttpClient = class {
1954
1970
  once: true
1955
1971
  });
1956
1972
  try {
1973
+ const requestBody = options?.formData !== void 0 ? typeof options.formData === "function" ? options.formData() : options.formData : options?.body !== void 0 ? JSON.stringify(options.body) : void 0;
1974
+ fetchAttempts += 1;
1975
+ lastRequestUrl = candidateUrl;
1957
1976
  const response = await fetch(candidateUrl, {
1958
1977
  method,
1959
1978
  headers,
1960
- body: options?.formData !== void 0 ? typeof options.formData === "function" ? options.formData() : options.formData : options?.body !== void 0 ? JSON.stringify(options.body) : void 0,
1979
+ body: requestBody,
1961
1980
  signal: controller.signal
1962
1981
  });
1963
1982
  options?.onResponse?.(response);
@@ -2061,6 +2080,7 @@ var HttpClient = class {
2061
2080
  return parsed;
2062
2081
  } catch (error) {
2063
2082
  clearTimeout(timeoutId);
2083
+ networkFailedAt = Date.now();
2064
2084
  if (options?.signal?.aborted) {
2065
2085
  lastError = requestCancelledError(options.signal);
2066
2086
  break requestAttempts;
@@ -2091,6 +2111,19 @@ var HttpClient = class {
2091
2111
  lastError,
2092
2112
  describeRequestTarget(path)
2093
2113
  );
2114
+ const failedAt = networkFailedAt ?? Date.now();
2115
+ const diagnosticUrl = networkDiagnosticUrl(lastRequestUrl);
2116
+ const networkDetails = {
2117
+ ...mappedNetworkError.details,
2118
+ request: {
2119
+ method,
2120
+ ...diagnosticUrl === void 0 ? {} : { url: diagnosticUrl },
2121
+ failedAtUtc: new Date(failedAt).toISOString(),
2122
+ // Elapsed time covers the entire logical request, including retries.
2123
+ elapsedMs: Math.max(0, failedAt - requestStartedAt),
2124
+ attempts: fetchAttempts
2125
+ }
2126
+ };
2094
2127
  if (options?.toolId) {
2095
2128
  const code = mappedNetworkError.code;
2096
2129
  throw withErrorCause(
@@ -2111,7 +2144,7 @@ var HttpClient = class {
2111
2144
  retryAfterMs: null,
2112
2145
  networkKind: code === "NETWORK_TIMEOUT" ? "timeout" : code === "NETWORK_ABORTED" ? "unknown" : "unavailable",
2113
2146
  networkScope: "client_to_deepline",
2114
- details: mappedNetworkError.details,
2147
+ details: networkDetails,
2115
2148
  publicDetails: options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : null
2116
2149
  }
2117
2150
  ),
@@ -2123,7 +2156,7 @@ var HttpClient = class {
2123
2156
  withCoworkNetworkHint(mappedNetworkError.message),
2124
2157
  void 0,
2125
2158
  mappedNetworkError.code,
2126
- mappedNetworkError.details
2159
+ networkDetails
2127
2160
  ),
2128
2161
  lastError
2129
2162
  );
@@ -149,7 +149,7 @@ type SdkRelease = {
149
149
  supportPolicy: SdkSupportPolicy;
150
150
  };
151
151
  declare const SDK_RELEASE: {
152
- readonly version: "0.3.234";
152
+ readonly version: "0.3.236";
153
153
  readonly updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.";
154
154
  readonly packageCapabilities: {
155
155
  readonly updatePreferences: 1;
package/dist/release.d.ts CHANGED
@@ -149,7 +149,7 @@ type SdkRelease = {
149
149
  supportPolicy: SdkSupportPolicy;
150
150
  };
151
151
  declare const SDK_RELEASE: {
152
- readonly version: "0.3.234";
152
+ readonly version: "0.3.236";
153
153
  readonly updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.";
154
154
  readonly packageCapabilities: {
155
155
  readonly updatePreferences: 1;
package/dist/release.js CHANGED
@@ -74,7 +74,7 @@ var SDK_RELEASE = {
74
74
  // getters keep their established compatibility behavior.
75
75
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
76
76
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
77
- version: "0.3.234",
77
+ version: "0.3.236",
78
78
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
79
79
  packageCapabilities: {
80
80
  updatePreferences: 1
package/dist/release.mjs CHANGED
@@ -48,7 +48,7 @@ var SDK_RELEASE = {
48
48
  // getters keep their established compatibility behavior.
49
49
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
50
50
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
51
- version: "0.3.234",
51
+ version: "0.3.236",
52
52
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
53
53
  packageCapabilities: {
54
54
  updatePreferences: 1
@@ -737,7 +737,7 @@ body {
737
737
  top: -24px;
738
738
  z-index: 20;
739
739
  display: grid;
740
- grid-template-columns: minmax(180px, 1.2fr) minmax(170px, 1.2fr) repeat(4, minmax(120px, .75fr)) auto auto;
740
+ grid-template-columns: minmax(180px, 1.2fr) minmax(170px, 1.2fr) repeat(5, minmax(120px, .75fr)) auto auto;
741
741
  gap: 8px;
742
742
  align-items: end;
743
743
  margin: -24px -32px 18px;
@@ -902,6 +902,7 @@ body {
902
902
  .expectation-result.pass { color: var(--green); }
903
903
  .expectation-result.fail { color: var(--red); }
904
904
  .expectation-result.unknown { color: var(--yellow); }
905
+ .grading-detail { margin-top: 3px; color: var(--text-dim); white-space: pre-wrap; }
905
906
  .expectation-failure-reason {
906
907
  margin-top: 5px;
907
908
  color: color-mix(in srgb, var(--red) 80%, var(--text-dim));
@@ -1371,11 +1371,12 @@ let isLive = false;
1371
1371
  let liveIntervalId = null;
1372
1372
  let currentEval = '';
1373
1373
  let currentSelection = 'all';
1374
- let sliceBy = 'run';
1374
+ let sliceBy = 'none';
1375
1375
  let runFilter = '';
1376
1376
  let agentFilter = 'all';
1377
1377
  let modelFilter = 'all';
1378
1378
  let skillFilter = 'all';
1379
+ let treatmentFilter = 'all';
1379
1380
  let visibleEvalKeys = [];
1380
1381
  let visibleSelectionKeys = [];
1381
1382
 
@@ -1444,6 +1445,7 @@ function attachEvalMetadata() {
1444
1445
  agent: firstPresent([metric && metric.agent], inferAgent(session.label, session.meta)),
1445
1446
  model: firstPresent([metric && metric.model_id, metric && metric.requested_model, metric && metric.model], inferModel(session.label, session.meta)),
1446
1447
  skill: firstPresent([metric && metric.invoke_skill], inferSkill(session)),
1448
+ treatment: firstPresent([metric && metric.variant_id], 'Unrecorded treatment'),
1447
1449
  };
1448
1450
  });
1449
1451
  }
@@ -1457,7 +1459,7 @@ function sessionsForCurrentEval() {
1457
1459
  }
1458
1460
 
1459
1461
  function selectionDimensions() {
1460
- const dimensions = ['run', 'agent', 'model', 'skill'];
1462
+ const dimensions = ['run', 'agent', 'model', 'skill', 'treatment'];
1461
1463
  return sliceBy === 'none' ? dimensions : dimensions.filter(dimension => dimension !== sliceBy);
1462
1464
  }
1463
1465
 
@@ -1471,6 +1473,7 @@ function selectionLabel(session) {
1471
1473
  agent: value => value,
1472
1474
  model: value => value,
1473
1475
  skill: value => value,
1476
+ treatment: value => value,
1474
1477
  };
1475
1478
  return selectionDimensions().map(dimension => labels[dimension](session.dimensions[dimension])).join(' · ') || 'All transcripts';
1476
1479
  }
@@ -1481,7 +1484,8 @@ function matchesTopFilters(session) {
1481
1484
  if (agentFilter !== 'all' && d.agent !== agentFilter) return false;
1482
1485
  if (modelFilter !== 'all' && d.model !== modelFilter) return false;
1483
1486
  if (skillFilter !== 'all' && d.skill !== skillFilter) return false;
1484
- return !query || [session.label, d.eval, d.run, d.agent, d.model, d.skill].join(' ').toLowerCase().includes(query);
1487
+ if (treatmentFilter !== 'all' && d.treatment !== treatmentFilter) return false;
1488
+ return !query || [session.label, d.eval, d.run, d.agent, d.model, d.skill, d.treatment].join(' ').toLowerCase().includes(query);
1485
1489
  }
1486
1490
 
1487
1491
  function matchingConfigurations() {
@@ -1525,6 +1529,7 @@ function saveViewerState() {
1525
1529
  agentFilter: agentFilter,
1526
1530
  modelFilter: modelFilter,
1527
1531
  skillFilter: skillFilter,
1532
+ treatmentFilter: treatmentFilter,
1528
1533
  }));
1529
1534
  }
1530
1535
 
@@ -1563,14 +1568,15 @@ function init() {
1563
1568
  currentEval = saved && visibleEvalKeys.includes(saved.currentEval)
1564
1569
  ? saved.currentEval
1565
1570
  : ((SESSIONS[Math.max(0, initialSession)] || {}).dimensions || {}).eval || visibleEvalKeys[0] || '';
1566
- sliceBy = saved && ['none', 'run', 'agent', 'model', 'skill'].includes(saved.sliceBy)
1571
+ sliceBy = saved && ['none', 'run', 'agent', 'model', 'skill', 'treatment'].includes(saved.sliceBy)
1567
1572
  ? saved.sliceBy
1568
- : (SESSIONS.length > 1 ? 'run' : 'none');
1573
+ : 'none';
1569
1574
  currentSelection = (saved && (saved.currentSelection || saved.currentRun)) || 'all';
1570
1575
  runFilter = (saved && saved.runFilter) || '';
1571
1576
  agentFilter = (saved && saved.agentFilter) || 'all';
1572
1577
  modelFilter = (saved && saved.modelFilter) || 'all';
1573
1578
  skillFilter = (saved && saved.skillFilter) || 'all';
1579
+ treatmentFilter = (saved && saved.treatmentFilter) || 'all';
1574
1580
 
1575
1581
  if (SESSIONS.length > 1) {
1576
1582
  document.getElementById('sidebar').classList.remove('hidden');
@@ -1720,6 +1726,7 @@ function switchEval(index) {
1720
1726
  agentFilter = 'all';
1721
1727
  modelFilter = 'all';
1722
1728
  skillFilter = 'all';
1729
+ treatmentFilter = 'all';
1723
1730
  renderSidebar();
1724
1731
  renderRunSidebar();
1725
1732
  renderWorkspace();
@@ -1744,7 +1751,7 @@ function switchSession(i) {
1744
1751
 
1745
1752
  function switchToCompare() {
1746
1753
  currentSelection = 'all';
1747
- sliceBy = 'run';
1754
+ sliceBy = 'none';
1748
1755
  renderRunSidebar();
1749
1756
  renderWorkspace();
1750
1757
  }
@@ -1760,14 +1767,17 @@ function renderWorkspaceControls() {
1760
1767
  const agents = uniqueValues(sessions.map(s => s.dimensions.agent));
1761
1768
  const models = uniqueValues(sessions.map(s => s.dimensions.model));
1762
1769
  const skills = uniqueValues(sessions.map(s => s.dimensions.skill));
1770
+ const treatments = uniqueValues(sessions.map(s => s.dimensions.treatment));
1763
1771
  return `<section class="workspace-controls" aria-label="Transcript filters">
1764
1772
  <div class="workspace-context"><span class="eyebrow">Evaluation</span><strong>${esc(currentEval)}</strong></div>
1765
1773
  <label class="control-field control-search"><span>Filter runs</span><input id="run-filter" value="${esc(runFilter)}" placeholder="model, skill, label…" oninput="runFilter=this.value" onkeydown="if(event.key==='Enter'){applyWorkspaceFilters()}"></label>
1766
1774
  <label class="control-field"><span>Agent</span><select id="agent-filter" onchange="agentFilter=this.value;applyWorkspaceFilters()">${optionList(agents, agentFilter, 'All agents')}</select></label>
1767
1775
  <label class="control-field"><span>Model</span><select id="model-filter" onchange="modelFilter=this.value;applyWorkspaceFilters()">${optionList(models, modelFilter, 'All models')}</select></label>
1768
1776
  <label class="control-field"><span>Skill</span><select id="skill-filter" onchange="skillFilter=this.value;applyWorkspaceFilters()">${optionList(skills, skillFilter, 'All skills')}</select></label>
1777
+ <label class="control-field"><span>Treatment</span><select id="treatment-filter" onchange="treatmentFilter=this.value;applyWorkspaceFilters()">${optionList(treatments, treatmentFilter, 'All treatments')}</select></label>
1769
1778
  <label class="control-field slice-control"><span>Slice by</span><select id="slice-by" onchange="changeSlice(this.value)">
1770
1779
  <option value="none" ${sliceBy === 'none' ? 'selected' : ''}>No slicing</option>
1780
+ <option value="treatment" ${sliceBy === 'treatment' ? 'selected' : ''}>Treatment</option>
1771
1781
  <option value="run" ${sliceBy === 'run' ? 'selected' : ''}>Run number</option>
1772
1782
  <option value="agent" ${sliceBy === 'agent' ? 'selected' : ''}>Agent</option>
1773
1783
  <option value="model" ${sliceBy === 'model' ? 'selected' : ''}>Model</option>
@@ -1790,6 +1800,9 @@ function resetWorkspaceFilters() {
1790
1800
  agentFilter = 'all';
1791
1801
  modelFilter = 'all';
1792
1802
  skillFilter = 'all';
1803
+ treatmentFilter = 'all';
1804
+ sliceBy = 'none';
1805
+ currentSelection = 'all';
1793
1806
  renderRunSidebar();
1794
1807
  renderWorkspace();
1795
1808
  }
@@ -1801,12 +1814,43 @@ function changeSlice(value) {
1801
1814
  renderWorkspace();
1802
1815
  }
1803
1816
 
1817
+ function renderSavedGrading(grading) {
1818
+ if (!grading || typeof grading !== 'object') return '';
1819
+ const outcome = grading.result || {};
1820
+ const graders = Array.isArray(grading.graders) ? grading.graders : [];
1821
+ if (!grading.result && !graders.length) return '';
1822
+ // Quality is an outcome, not a pass/fail check. In particular, zero is a
1823
+ // real Low score; missing evidence and judge errors must remain ungraded.
1824
+ const quality = outcome.score != null
1825
+ ? `${outcome.score}/${outcome.max_score ?? 2} (${outcome.grade || 'scored'})`
1826
+ : `Not scored (${outcome.status || 'unknown'})`;
1827
+ const checks = outcome.checks_status || 'unknown';
1828
+ const rows = graders.map(item => {
1829
+ const status = item.status || 'unknown';
1830
+ const result = item.score != null
1831
+ ? `${item.score} (${item.grade || status})`
1832
+ : status;
1833
+ const resultClass = item.type === 'deterministic' && status === 'passed'
1834
+ ? 'pass' : (status === 'failed' || status === 'error' ? 'fail' : (item.score != null ? '' : 'unknown'));
1835
+ const errors = Array.isArray(item.evidence_errors) ? item.evidence_errors : [];
1836
+ const details = [item.reason, item.error_kind, ...errors.map(error => [error.code, error.detail].filter(Boolean).join(': '))].filter(Boolean);
1837
+ const reason = details.length ? `<div class="${status === 'failed' || status === 'error' ? 'expectation-failure-reason' : 'grading-detail'}">${esc(details.join(' · '))}</div>` : '';
1838
+ return `<tr><td>${esc(item.id || 'Unnamed grader')}${reason}</td><td>${esc(item.type || 'unknown')}</td><td><span class="expectation-result ${resultClass}">${esc(result)}</span></td></tr>`;
1839
+ }).join('');
1840
+ return `<div class="expectations-mini${checks === 'failed' || checks === 'error' ? ' has-failures' : ''}">
1841
+ <div class="expectations-mini-heading"><span>Outcome quality</span><strong>${esc(quality)}</strong></div>
1842
+ <div class="expectations-mini-heading"><span>Required checks: ${esc(checks)}</span><span>Grading: ${esc(outcome.status || 'unknown')}</span></div>
1843
+ ${rows ? `<div class="expectations-table-wrap"><table><thead><tr><th>Grader</th><th>Type</th><th>Result</th></tr></thead><tbody>${rows}</tbody></table></div>` : ''}
1844
+ </div>`;
1845
+ }
1846
+
1804
1847
  function renderExpectationMiniTable(session) {
1805
1848
  const metric = session.evalMetric;
1849
+ const grading = renderSavedGrading(metric && metric.grading);
1806
1850
  const expectations = metric && Array.isArray(metric.expectations) ? metric.expectations : [];
1807
1851
  const summary = metric && metric.expectation_summary;
1808
1852
  if (!expectations.length) {
1809
- return `<div class="expectations-empty"><span>Eval results</span><strong>Pending or unavailable</strong></div>`;
1853
+ return grading || `<div class="expectations-empty"><span>Eval results</span><strong>Pending or unavailable</strong></div>`;
1810
1854
  }
1811
1855
  const passed = summary ? summary.passed : expectations.filter(item => String(item.result || '').toUpperCase() === 'PASS').length;
1812
1856
  const total = summary ? summary.total : expectations.length;
@@ -1820,7 +1864,7 @@ function renderExpectationMiniTable(session) {
1820
1864
  : '';
1821
1865
  return `<tr><td>${esc(item.expectation || '—')}${reason}</td><td>${esc(item.category || 'other')}</td><td><span class="expectation-result ${resultClass}">${esc(result)}</span></td></tr>`;
1822
1866
  }).join('');
1823
- return `<div class="expectations-mini ${statusClass}">
1867
+ return grading + `<div class="expectations-mini ${statusClass}">
1824
1868
  <div class="expectations-mini-heading"><span>Eval results</span><strong>${passed}/${total} passed</strong></div>
1825
1869
  <div class="expectations-table-wrap"><table><thead><tr><th>Expectation</th><th>Category</th><th>Result</th></tr></thead><tbody>${rows}</tbody></table></div>
1826
1870
  </div>`;
@@ -1887,6 +1931,14 @@ function renderFinalArtifacts(session) {
1887
1931
  </section>`;
1888
1932
  }
1889
1933
 
1934
+ function renderSourceProvenance(metric) {
1935
+ if (!metric.bundle_digest) return '';
1936
+ const fields = { 'Source run': metric.source_run, 'Bundle': metric.bundle_digest, 'Assignment': metric.assignment_id };
1937
+ return '<details class="lane-prompt"><summary>Source provenance</summary><div class="prompt-text">'
1938
+ + Object.entries(fields).filter(([, value]) => value).map(([key, value]) => esc(key + ': ' + value)).join('<br>')
1939
+ + '</div></details>';
1940
+ }
1941
+
1890
1942
  function renderLaneSession(session) {
1891
1943
  const i = session.sourceIndex;
1892
1944
  const d = session.dimensions;
@@ -1900,9 +1952,10 @@ function renderLaneSession(session) {
1900
1952
  let html = `<article class="lane-session">
1901
1953
  <header class="lane-session-header">
1902
1954
  <div class="lane-title-row"><div><span class="run-kicker">Run ${esc(d.run)}</span><h3>${esc(d.agent)} / ${esc(d.model)}</h3></div><button class="download-btn" onclick="downloadJsonl(${i})">JSONL</button></div>
1903
- <div class="dimension-chips"><span>${esc(d.skill)}</span><span>${formatDuration(session.stats.duration_s)}</span><span>${session.stats.tool_calls} calls</span><span class="${session.stats.tool_errors ? 'chip-error' : ''}">${session.stats.tool_errors} errors${errorRate ? ' · ' + errorRate + '%' : ''}</span>${embeddedFailures ? `<span class="chip-error">${embeddedFailures} embedded failures</span>` : ''}${infraAffected ? `<span class="chip-error">infra affected${infraCodes ? ' · ' + esc(infraCodes) : ''}</span>` : ''}</div>
1955
+ <div class="dimension-chips"><span>${esc(d.treatment)}</span><span>${esc(d.skill)}</span><span>${formatDuration(session.stats.duration_s)}</span><span>${session.stats.tool_calls} calls</span><span class="${session.stats.tool_errors ? 'chip-error' : ''}">${session.stats.tool_errors} errors${errorRate ? ' · ' + errorRate + '%' : ''}</span>${embeddedFailures ? `<span class="chip-error">${embeddedFailures} embedded failures</span>` : ''}${infraAffected ? `<span class="chip-error">infra affected${infraCodes ? ' · ' + esc(infraCodes) : ''}</span>` : ''}</div>
1904
1956
  ${renderExpectationMiniTable(session)}
1905
1957
  ${renderInfraSignals(metric)}
1958
+ ${renderSourceProvenance(metric)}
1906
1959
  </header>`;
1907
1960
  if (!session.transcript_available) {
1908
1961
  html += '<div class="transcript-unavailable">No transcript was retained for this completed eval. Its scored expectations remain included in this comparison.</div>';
@@ -1985,6 +2038,7 @@ function renderSession(i) {
1985
2038
  html += item('Agent', s.dimensions.agent);
1986
2039
  html += item('Model', s.dimensions.model || s.meta.model || '\u2014');
1987
2040
  html += item('Skill', s.dimensions.skill);
2041
+ html += item('Treatment', s.dimensions.treatment);
1988
2042
  html += item('Run', s.dimensions.run);
1989
2043
  html += item('Session', (s.meta.session_id || '\u2014').slice(0, 12));
1990
2044
  if (s.meta.uploaded_by) html += item('Uploaded by', s.meta.uploaded_by);
@@ -2013,6 +2067,7 @@ function renderSession(i) {
2013
2067
  if (s.stats.loop_groups > 0) html += item('Retry Loops', s.stats.loop_groups);
2014
2068
  if (s.stats.error_streak_groups > 0) html += item('Error Streaks', s.stats.error_streak_groups + ' (max ' + s.stats.max_error_streak + ')');
2015
2069
  html += '</div>';
2070
+ html += renderSourceProvenance(metric);
2016
2071
  html += renderExpectationMiniTable(s);
2017
2072
  html += renderInfraSignals(metric);
2018
2073
  if (!s.transcript_available) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.3.234",
3
+ "version": "0.3.236",
4
4
  "description": "GTM data CLI and TypeScript SDK for coding agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://code.deepline.com",