deepline 0.2.1 → 0.2.2

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.
@@ -744,7 +744,10 @@ export type MonitorUpdateChangeSummary = {
744
744
  };
745
745
  upstream: {
746
746
  resource_replaced: boolean;
747
- strategy: 'unchanged' | 'create_then_delete_previous';
747
+ strategy:
748
+ | 'unchanged'
749
+ | 'create_then_delete_previous'
750
+ | 'deferred_until_reactivation';
748
751
  };
749
752
  };
750
753
  export type MonitorUpdateResult = {
@@ -160,7 +160,7 @@ export const SDK_RELEASE = {
160
160
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
161
161
  // exposed storage-dependent synchronous access. This deliberate minor
162
162
  // release keeps lazy paging semantics independent of row residency.
163
- version: '0.2.1',
163
+ version: '0.2.2',
164
164
  contracts: {
165
165
  api: {
166
166
  name: 'sdk-http-api',
@@ -0,0 +1,95 @@
1
+ import { redactTelemetryText } from './redaction';
2
+
3
+ const MAX_ERROR_SUMMARY_LENGTH = 900;
4
+
5
+ const SECRET_ASSIGNMENT =
6
+ /\b((?:[A-Za-z][A-Za-z0-9_-]*[_-])?(?:token|secret|password|api[_ -]?key|access[_ -]?key))\b(\s*[:=]\s*)([^\s,;]+)/gi;
7
+ const AUTHORIZATION_VALUE =
8
+ /\bauthorization\b(\s*[:=]\s*)(?:Bearer\s+)?[^\s,;|]+/gi;
9
+ const BEARER_TOKEN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
10
+ const SLACK_WEBHOOK =
11
+ /https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/_-]+/gi;
12
+ const URL_SECRET = /([?&](?:token|secret|key|signature|sig|code)=)[^&\s]+/gi;
13
+ const BARE_CREDENTIAL_PATTERNS = [
14
+ /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/gi,
15
+ /\bgithub_pat_[A-Za-z0-9_]{20,}\b/gi,
16
+ /\b(?:xaat|xain)-[A-Za-z0-9_-]{16,}\b/gi,
17
+ /\bsk-ant-(?:api|oat)[A-Za-z0-9_-]{8,}\b/gi,
18
+ /\bxox(?:a|b|p|r|s)-[A-Za-z0-9-]{10,}\b/gi,
19
+ ] as const;
20
+
21
+ export type ScheduledJobFailure = {
22
+ jobId: string;
23
+ jobName: string;
24
+ scheduler: string;
25
+ error: unknown;
26
+ runUrl?: string | null;
27
+ stage?: string | null;
28
+ attempt?: number | null;
29
+ occurredAt?: string;
30
+ test?: boolean;
31
+ };
32
+
33
+ export function sanitizeScheduledJobError(
34
+ error: unknown,
35
+ maxLength = MAX_ERROR_SUMMARY_LENGTH,
36
+ ): string {
37
+ const raw =
38
+ error instanceof Error
39
+ ? `${error.name}: ${error.message}`
40
+ : typeof error === 'string'
41
+ ? error
42
+ : safeJson(error);
43
+ let compact = redactTelemetryText(raw)
44
+ .replace(/\u001b\[[0-9;]*m/g, '')
45
+ .replace(/\r/g, '')
46
+ .split('\n')
47
+ .map((line) => line.trim())
48
+ .filter(Boolean)
49
+ .join(' | ')
50
+ .replace(
51
+ AUTHORIZATION_VALUE,
52
+ (_match, separator) => `Authorization${separator}[REDACTED]`,
53
+ )
54
+ .replace(BEARER_TOKEN, 'Bearer [REDACTED]')
55
+ .replace(SLACK_WEBHOOK, '[REDACTED_SLACK_WEBHOOK]')
56
+ .replace(SECRET_ASSIGNMENT, (_match, label, separator) => {
57
+ return `${label}${separator}[REDACTED]`;
58
+ })
59
+ .replace(URL_SECRET, '$1[REDACTED]')
60
+ .replace(/\s+/g, ' ')
61
+ .trim();
62
+ for (const pattern of BARE_CREDENTIAL_PATTERNS) {
63
+ compact = compact.replace(pattern, '[REDACTED_CREDENTIAL]');
64
+ }
65
+ if (!compact) return 'No error detail was captured.';
66
+ return compact.length <= maxLength
67
+ ? compact
68
+ : `${compact.slice(0, Math.max(0, maxLength - 1))}…`;
69
+ }
70
+
71
+ export function formatScheduledJobFailure(input: ScheduledJobFailure): string {
72
+ const title = input.test
73
+ ? '[TEST] Scheduled job failure alert'
74
+ : 'Scheduled job failed';
75
+ return [
76
+ title,
77
+ `Job: ${input.jobName} (${input.jobId})`,
78
+ `Scheduler: ${input.scheduler}`,
79
+ input.stage ? `Stage: ${input.stage}` : null,
80
+ input.attempt ? `Attempt: ${input.attempt}` : null,
81
+ `Error: ${sanitizeScheduledJobError(input.error)}`,
82
+ `Occurred: ${input.occurredAt ?? new Date().toISOString()}`,
83
+ input.runUrl ? `Diagnostics: ${input.runUrl}` : null,
84
+ ]
85
+ .filter((line): line is string => Boolean(line))
86
+ .join('\n');
87
+ }
88
+
89
+ function safeJson(value: unknown): string {
90
+ try {
91
+ return JSON.stringify(value) ?? String(value);
92
+ } catch {
93
+ return String(value);
94
+ }
95
+ }
@@ -9,6 +9,7 @@ import {
9
9
  STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS,
10
10
  validatePlaySandboxRuntimeLimits,
11
11
  } from '@shared_libs/play-runtime/sandbox-runtime-limits';
12
+ import type { PlayRunnerRuntimeLifecycleEvent } from '../types';
12
13
 
13
14
  const DAYTONA_CREATE_TIMEOUT_SECONDS = 10;
14
15
  const DAYTONA_CREATE_RETRY_DELAYS_MS = [0, 500, 1_500] as const;
@@ -41,6 +42,10 @@ export type DaytonaStageEmitter = (
41
42
  extra?: Record<string, unknown>,
42
43
  ) => void;
43
44
 
45
+ type DaytonaCreateCallObserver = (
46
+ event: PlayRunnerRuntimeLifecycleEvent,
47
+ ) => Promise<void>;
48
+
44
49
  export type AcquiredDaytonaSandbox = {
45
50
  sandbox: DaytonaSandbox;
46
51
  daytonaOrganizationId: string;
@@ -104,6 +109,16 @@ type DaytonaCreateResult = {
104
109
  attemptElapsedMs: number;
105
110
  };
106
111
 
112
+ function daytonaCreateErrorClass(
113
+ error: unknown,
114
+ ): 'timeout' | 'capacity' | 'rate_limit' | 'other' {
115
+ const message = error instanceof Error ? error.message : String(error);
116
+ if (/timed out|timeout/i.test(message)) return 'timeout';
117
+ if (/total cpu limit|capacity/i.test(message)) return 'capacity';
118
+ if (/too many requests|rate limit|429/i.test(message)) return 'rate_limit';
119
+ return 'other';
120
+ }
121
+
107
122
  async function rejectAcquiredSandbox(
108
123
  sandbox: DaytonaSandbox,
109
124
  reason: string,
@@ -307,11 +322,13 @@ async function createRetriedOneShotDaytonaSandbox(input: {
307
322
  orgId: string;
308
323
  context: DaytonaExecutionContext;
309
324
  emitStage: DaytonaStageEmitter;
325
+ observeCreateCall?: DaytonaCreateCallObserver;
326
+ nextProviderAttempt: () => number;
310
327
  startedAt: number;
311
328
  }): Promise<DaytonaCreateResult> {
312
329
  const errors: string[] = [];
313
330
  for (const [index, delayMs] of DAYTONA_CREATE_RETRY_DELAYS_MS.entries()) {
314
- const attempt = index + 1;
331
+ const attempt = input.nextProviderAttempt();
315
332
  const sandboxName = `dl-${crypto.randomUUID()}`;
316
333
  if (delayMs > 0) {
317
334
  input.emitStage('create:retry', {
@@ -323,19 +340,26 @@ async function createRetriedOneShotDaytonaSandbox(input: {
323
340
  await new Promise((resolve) => setTimeout(resolve, delayMs));
324
341
  }
325
342
  const attemptStartedAt = Date.now();
343
+ await input.observeCreateCall?.({
344
+ type: 'daytona_create_call_started',
345
+ occurredAtMs: attemptStartedAt,
346
+ providerAttempt: attempt,
347
+ });
348
+ let sandbox: DaytonaSandbox;
326
349
  try {
327
- const sandbox = await createOneShotDaytonaSandbox({
350
+ sandbox = await createOneShotDaytonaSandbox({
328
351
  daytona: input.daytona,
329
352
  orgId: input.orgId,
330
353
  context: input.context,
331
354
  sandboxName,
332
355
  });
333
- return {
334
- sandbox,
335
- attempt,
336
- attemptElapsedMs: Date.now() - attemptStartedAt,
337
- };
338
356
  } catch (error) {
357
+ await input.observeCreateCall?.({
358
+ type: 'daytona_create_call_failed',
359
+ occurredAtMs: Date.now(),
360
+ providerAttempt: attempt,
361
+ errorClass: daytonaCreateErrorClass(error),
362
+ });
339
363
  const message = error instanceof Error ? error.message : String(error);
340
364
  errors.push(message);
341
365
  input.emitStage('create:attempt_failed', {
@@ -366,7 +390,38 @@ async function createRetriedOneShotDaytonaSandbox(input: {
366
390
  { cause: error },
367
391
  );
368
392
  }
393
+ continue;
369
394
  }
395
+ // The outcome must be attributed immediately after Daytona acknowledges
396
+ // creation, before resource-policy validation can reject it. Otherwise a
397
+ // created-but-rejected sandbox is indistinguishable from an unknown
398
+ // create result. Do not turn a failed *post-create* journal write into a
399
+ // new provider create: the sandbox is already real. The missing durable
400
+ // outcome is deliberately loud in worker logs and makes the capture gate
401
+ // fail as incomplete telemetry, while the normal resource ledger still
402
+ // records the known sandbox for cleanup below.
403
+ const acquiredAt = Date.now();
404
+ try {
405
+ await input.observeCreateCall?.({
406
+ type: 'daytona_create_call_succeeded',
407
+ occurredAtMs: acquiredAt,
408
+ providerAttempt: attempt,
409
+ sandboxId: sandbox.id,
410
+ });
411
+ } catch {
412
+ console.error('[play-runner.daytona.create_lifecycle_event_unrecorded]', {
413
+ workflowId: input.context.workflowId ?? null,
414
+ runId: input.context.runId ?? null,
415
+ attempt,
416
+ sandboxId: sandbox.id,
417
+ eventType: 'daytona_create_call_succeeded',
418
+ });
419
+ }
420
+ return {
421
+ sandbox,
422
+ attempt,
423
+ attemptElapsedMs: acquiredAt - attemptStartedAt,
424
+ };
370
425
  }
371
426
  const message = `Daytona sandbox create failed across ${errors.length} bounded attempts: ${errors.join('; ')}`;
372
427
  const fallbackReason =
@@ -385,6 +440,8 @@ async function acquireOneShotDaytonaSandbox(input: {
385
440
  orgId: string;
386
441
  context: DaytonaExecutionContext;
387
442
  emitStage: DaytonaStageEmitter;
443
+ observeCreateCall?: DaytonaCreateCallObserver;
444
+ nextProviderAttempt: () => number;
388
445
  startedAt: number;
389
446
  }): Promise<AcquiredDaytonaSandbox> {
390
447
  const limits = validatePlaySandboxRuntimeLimits(
@@ -464,11 +521,13 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
464
521
  daytona: DaytonaClient;
465
522
  context: DaytonaExecutionContext;
466
523
  emitStage: DaytonaStageEmitter;
524
+ observeCreateCall?: DaytonaCreateCallObserver;
467
525
  startedAt?: number;
468
526
  }): OneShotDaytonaSandboxLifecycle {
469
527
  const orgId = validateDaytonaExecutionContext(input.context);
470
528
  const startedAt = input.startedAt ?? Date.now();
471
529
  let disposed = false;
530
+ let providerAttempt = 0;
472
531
  const acquiredSandboxes = new Map<string, AcquiredDaytonaSandbox>();
473
532
  let latestAcquiredSandboxPromise: Promise<AcquiredDaytonaSandbox>;
474
533
  const createFreshSandbox = () => {
@@ -477,6 +536,11 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
477
536
  orgId,
478
537
  context: input.context,
479
538
  emitStage: input.emitStage,
539
+ observeCreateCall: input.observeCreateCall,
540
+ nextProviderAttempt: () => {
541
+ providerAttempt += 1;
542
+ return providerAttempt;
543
+ },
480
544
  startedAt,
481
545
  }).then((acquired) => {
482
546
  acquiredSandboxes.set(acquired.sandbox.id, acquired);
@@ -670,6 +670,7 @@ function prepareDaytonaExecution(
670
670
  context: input.context,
671
671
  emitStage: (stage, extra) =>
672
672
  emitDaytonaStage(callbacks, input.context, stage, extra),
673
+ observeCreateCall: callbacks?.onRuntimeLifecycleEvent,
673
674
  });
674
675
  return {
675
676
  kind: 'daytona',
@@ -40,6 +40,22 @@ export type RuntimeResourceTerminalReason =
40
40
  | 'cancelled'
41
41
  | 'lease_expired';
42
42
 
43
+ /**
44
+ * Safe, provider-edge lifecycle evidence. The scheduler records these before
45
+ * relying on a non-idempotent provider outcome; raw provider responses,
46
+ * commands, inputs, and credentials are intentionally not part of the type.
47
+ */
48
+ export type PlayRunnerRuntimeLifecycleEvent = {
49
+ type:
50
+ | 'daytona_create_call_started'
51
+ | 'daytona_create_call_succeeded'
52
+ | 'daytona_create_call_failed';
53
+ occurredAtMs: number;
54
+ providerAttempt: number;
55
+ sandboxId?: string;
56
+ errorClass?: 'timeout' | 'capacity' | 'rate_limit' | 'other';
57
+ };
58
+
43
59
  export class RuntimeResourceFenceLostError extends Error {
44
60
  constructor(message: string) {
45
61
  super(message);
@@ -55,6 +71,13 @@ export interface PlayRunnerCallbacks {
55
71
  onRuntimeResourceAcquired?: (
56
72
  resource: PlayRunnerRuntimeResource,
57
73
  ) => void | Promise<void>;
74
+ /**
75
+ * Durable scheduler-owned evidence for a provider create call. Backends
76
+ * await this callback at the create boundary; it is not best-effort logging.
77
+ */
78
+ onRuntimeLifecycleEvent?: (
79
+ event: PlayRunnerRuntimeLifecycleEvent,
80
+ ) => Promise<void>;
58
81
  /**
59
82
  * Scheduler-owned readiness read for a detached runner attempt. Daytona uses
60
83
  * this direct control-plane port before parking; it is never serialized into
package/dist/cli/index.js CHANGED
@@ -1040,7 +1040,7 @@ var SDK_RELEASE = {
1040
1040
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
1041
1041
  // exposed storage-dependent synchronous access. This deliberate minor
1042
1042
  // release keeps lazy paging semantics independent of row residency.
1043
- version: "0.2.1",
1043
+ version: "0.2.2",
1044
1044
  contracts: {
1045
1045
  api: {
1046
1046
  name: "sdk-http-api",
@@ -1025,7 +1025,7 @@ var SDK_RELEASE = {
1025
1025
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
1026
1026
  // exposed storage-dependent synchronous access. This deliberate minor
1027
1027
  // release keeps lazy paging semantics independent of row residency.
1028
- version: "0.2.1",
1028
+ version: "0.2.2",
1029
1029
  contracts: {
1030
1030
  api: {
1031
1031
  name: "sdk-http-api",
package/dist/index.d.mts CHANGED
@@ -1992,7 +1992,7 @@ type MonitorUpdateChangeSummary = {
1992
1992
  };
1993
1993
  upstream: {
1994
1994
  resource_replaced: boolean;
1995
- strategy: 'unchanged' | 'create_then_delete_previous';
1995
+ strategy: 'unchanged' | 'create_then_delete_previous' | 'deferred_until_reactivation';
1996
1996
  };
1997
1997
  };
1998
1998
  type MonitorUpdateResult = {
package/dist/index.d.ts CHANGED
@@ -1992,7 +1992,7 @@ type MonitorUpdateChangeSummary = {
1992
1992
  };
1993
1993
  upstream: {
1994
1994
  resource_replaced: boolean;
1995
- strategy: 'unchanged' | 'create_then_delete_previous';
1995
+ strategy: 'unchanged' | 'create_then_delete_previous' | 'deferred_until_reactivation';
1996
1996
  };
1997
1997
  };
1998
1998
  type MonitorUpdateResult = {
package/dist/index.js CHANGED
@@ -763,7 +763,7 @@ var SDK_RELEASE = {
763
763
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
764
764
  // exposed storage-dependent synchronous access. This deliberate minor
765
765
  // release keeps lazy paging semantics independent of row residency.
766
- version: "0.2.1",
766
+ version: "0.2.2",
767
767
  contracts: {
768
768
  api: {
769
769
  name: "sdk-http-api",
package/dist/index.mjs CHANGED
@@ -689,7 +689,7 @@ var SDK_RELEASE = {
689
689
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
690
690
  // exposed storage-dependent synchronous access. This deliberate minor
691
691
  // release keeps lazy paging semantics independent of row residency.
692
- version: "0.2.1",
692
+ version: "0.2.2",
693
693
  contracts: {
694
694
  api: {
695
695
  name: "sdk-http-api",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {