deepline 0.3.141 → 0.3.143

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.
Files changed (25) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/sdk/src/types.ts +65 -2
  3. package/dist/bundling-sources/shared_libs/observability/dlq.ts +25 -0
  4. package/dist/bundling-sources/shared_libs/observability/queue-health.ts +111 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/bettercontact-batching.ts +34 -8
  6. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +271 -44
  7. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +25 -6
  8. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +132 -8
  9. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +3 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +49 -54
  11. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +14 -7
  12. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +118 -29
  13. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-queue-health.ts +183 -36
  14. package/dist/bundling-sources/shared_libs/product-notifications/events.ts +4 -1
  15. package/dist/cli/index.js +242 -12
  16. package/dist/cli/index.mjs +242 -12
  17. package/dist/index.d.mts +54 -2
  18. package/dist/index.d.ts +54 -2
  19. package/dist/index.js +1 -1
  20. package/dist/index.mjs +1 -1
  21. package/dist/release.d.mts +1 -1
  22. package/dist/release.d.ts +1 -1
  23. package/dist/release.js +1 -1
  24. package/dist/release.mjs +1 -1
  25. package/package.json +1 -1
@@ -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.141',
203
+ version: '0.3.143',
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: {
@@ -193,9 +193,9 @@ export type {
193
193
  *
194
194
  * Returned by {@link DeeplineClient.listTools} and ranked tool search. Use
195
195
  * `getTool(toolId)` or the matching HTTP describe route for provider-specific
196
- * schema, examples, pricing, and extraction guidance before executing.
196
+ * schema, examples, pricing, extraction guidance, and execution metadata.
197
197
  */
198
- export interface ToolDefinition {
198
+ export interface ToolDefinition extends ToolExecutionMetadataFields {
199
199
  /** Unique tool identifier used in API calls (e.g. `"dropleads_search_people"`). */
200
200
  toolId: string;
201
201
  /** Provider that backs this tool (e.g. `"hunter"`, `"dropleads"`, `"test"`). */
@@ -345,6 +345,45 @@ export interface ToolDefinition {
345
345
  connectionMessage?: string;
346
346
  }
347
347
 
348
+ export type ToolBatchCapability = {
349
+ status: 'compiled' | 'exposed_uncompiled' | 'upstream_only';
350
+ surface: 'registry' | 'local' | 'docs';
351
+ batchKind: 'identifier_batch' | 'query_share' | 'async_dataset_job';
352
+ batchOperation: string | null;
353
+ maxBatchSize: number | null;
354
+ notes: string;
355
+ };
356
+
357
+ /** Optional batching, pacing, and configured rate metadata for tool catalog entries. */
358
+ export interface ToolExecutionMetadataFields {
359
+ batchCapability?: ToolBatchCapability;
360
+ batch_capability?: ToolBatchCapability;
361
+ executionMetadata?: ToolExecutionMetadata;
362
+ execution_metadata?: ToolExecutionMetadata;
363
+ }
364
+
365
+ export type ToolExecutionMetadata = {
366
+ provider: string;
367
+ sourceOperation: string;
368
+ effectiveOperation: string;
369
+ batch: {
370
+ batchOperation: string | null;
371
+ maxBatchSize: number;
372
+ };
373
+ configuredRateLimits: Array<{
374
+ source: 'deepline_configuration';
375
+ scope: string;
376
+ requestsPerWindow: number;
377
+ windowMs: number;
378
+ usage: 'queue_hint' | 'enforced' | 'queue_hint_and_enforced' | 'disabled';
379
+ explicitMaxConcurrency: number | null;
380
+ }>;
381
+ queueHint: {
382
+ requestsPerSecondHint: number;
383
+ derivedConcurrency: number;
384
+ };
385
+ };
386
+
348
387
  export interface ModelProviderOptionField {
349
388
  name: string;
350
389
  type: 'string' | 'number' | 'boolean' | 'object' | 'array';
@@ -697,6 +736,11 @@ export interface PlayRunPackage {
697
736
  startedAt?: number | null;
698
737
  finishedAt?: number | null;
699
738
  durationMs?: number | null;
739
+ outcome?: PlayRunOutcome;
740
+ recovery?: {
741
+ mode: 'replayed' | 'forced' | 'recovered' | 'joined';
742
+ sourceRunId?: string;
743
+ };
700
744
  error?: string;
701
745
  /** Canonical explanation of what this run is doing or waiting on. */
702
746
  activity?: PlayRunActivityProjection | null;
@@ -742,6 +786,19 @@ export interface PlayRunPackage {
742
786
  };
743
787
  }
744
788
 
789
+ /**
790
+ * Stable classification of whether this run executed work or reused/recovered
791
+ * an existing durable run outcome.
792
+ */
793
+ export type PlayRunOutcome =
794
+ | 'executed'
795
+ | 'reused'
796
+ | 'joined'
797
+ | 'recovered'
798
+ | 'forced'
799
+ | 'waiting'
800
+ | 'failed';
801
+
745
802
  /**
746
803
  * Current status of a play execution, returned by {@link DeeplineClient.getPlayStatus}.
747
804
  *
@@ -779,6 +836,12 @@ export interface PlayStatus {
779
836
  | 'completed'
780
837
  | 'failed'
781
838
  | 'cancelled';
839
+ /** How this run was admitted or recovered, when the server can prove it. */
840
+ outcome?: PlayRunOutcome;
841
+ recovery?: {
842
+ mode: 'replayed' | 'forced' | 'recovered' | 'joined';
843
+ sourceRunId?: string;
844
+ };
782
845
  /** Execution progress with logs and error details. */
783
846
  progress?: PlayProgressStatus;
784
847
  /** Partial or final result. Available once the play returns. */
@@ -277,6 +277,8 @@ export type QueueItem = {
277
277
  readonly editableFields: readonly FieldDefinition[];
278
278
  readonly createdAt?: string;
279
279
  readonly updatedAt?: string;
280
+ /** Native failure timestamp when the owner can provide it. */
281
+ readonly failedAt?: string;
280
282
  readonly summary?: string;
281
283
  readonly retiredAt?: string;
282
284
  readonly retirementReason?: string;
@@ -328,6 +330,18 @@ export type QueueItemUnavailable = {
328
330
  readonly reason: string;
329
331
  };
330
332
 
333
+ /** Server attestation that an item page came from the owner's bounded,
334
+ * indexed failed-time reader rather than the generic item listing. */
335
+ export type QueueItemFailedTimeRange = {
336
+ readonly kind: 'failed-time-range';
337
+ readonly after: string | null;
338
+ readonly before: string;
339
+ readonly index: string;
340
+ readonly indexed: true;
341
+ /** Stable across every page in the same cursor-bound plan. */
342
+ readonly snapshotAt: string;
343
+ };
344
+
331
345
  export type QueueItemList = {
332
346
  readonly contractVersion: typeof QUEUE_ITEM_CONTROL_CONTRACT_VERSION;
333
347
  readonly queueId: string;
@@ -337,6 +351,8 @@ export type QueueItemList = {
337
351
  readonly nextCursor: string | null;
338
352
  readonly limit: number;
339
353
  readonly cursor: string | null;
354
+ /** Present only for a successfully measured indexed failure-time range. */
355
+ readonly failedTimeRange?: QueueItemFailedTimeRange;
340
356
  readonly unavailable?: QueueItemUnavailable;
341
357
  };
342
358
 
@@ -445,6 +461,14 @@ export type QueueItemConnector = {
445
461
  readonly source: string;
446
462
  readonly mapNativeState: (nativeState: string) => QueueItemState;
447
463
  readonly list: (context: QueueItemConnectorContext) => Promise<QueueItemList>;
464
+ /** Optional source-indexed failure-window listing for safe, bounded
465
+ * retirement planning. The lower bound is inclusive and the upper bound is
466
+ * exclusive. Connectors must not emulate this by scanning their ordinary
467
+ * item listing; unsupported sources leave the capability absent. */
468
+ readonly listFailedInRange?: (
469
+ context: QueueItemConnectorContext,
470
+ range: { after?: string; before: string },
471
+ ) => Promise<QueueItemList>;
448
472
  readonly get: (
449
473
  context: QueueItemConnectorContext,
450
474
  itemId: string,
@@ -470,6 +494,7 @@ export function normalizeQueueItemState(nativeState: string): QueueItemState {
470
494
  return 'running';
471
495
  }
472
496
  if (
497
+ state === 'failed' ||
473
498
  state === 'dead_lettered' ||
474
499
  state === 'dead-lettered' ||
475
500
  state === 'dlq'
@@ -363,6 +363,117 @@ export type QueueObservation = {
363
363
  readonly adapter: string;
364
364
  };
365
365
 
366
+ export type QueueRetirementCandidate = {
367
+ readonly itemId: string;
368
+ readonly bucket: 'blocked' | 'deadLettered';
369
+ readonly ageMs: number;
370
+ };
371
+
372
+ /**
373
+ * Remove operator-retired identities from a bounded source sample. The source
374
+ * still owns every row; the disposition overlay only changes what operators
375
+ * count as unresolved. Truncated samples remain lower bounds after filtering.
376
+ */
377
+ export function excludeRetiredQueueCandidates(input: {
378
+ observation: QueueObservation;
379
+ candidates: readonly QueueRetirementCandidate[];
380
+ retiredItemIds: ReadonlySet<string>;
381
+ truncatedBuckets?: ReadonlySet<QueueRetirementCandidate['bucket']>;
382
+ }): QueueObservation {
383
+ const {
384
+ observation,
385
+ candidates,
386
+ retiredItemIds,
387
+ truncatedBuckets = new Set(),
388
+ } = input;
389
+ const blocked = candidates.filter(
390
+ (candidate) => candidate.bucket === 'blocked',
391
+ );
392
+ const deadLettered = candidates.filter(
393
+ (candidate) => candidate.bucket === 'deadLettered',
394
+ );
395
+ const adjustDepth = (
396
+ bucket: 'blocked' | 'deadLettered',
397
+ rows: readonly QueueRetirementCandidate[],
398
+ ): Measured<number> => {
399
+ const measured = observation.depth[bucket];
400
+ if (measured.kind !== 'exact' && measured.kind !== 'lower_bound') {
401
+ return measured;
402
+ }
403
+ const activeSampleCount = rows.reduce(
404
+ (count, row) => count + Number(!retiredItemIds.has(row.itemId)),
405
+ 0,
406
+ );
407
+ if (
408
+ measured.kind === 'lower_bound' ||
409
+ truncatedBuckets.has(bucket) ||
410
+ rows.length !== measured.value
411
+ ) {
412
+ // For a partial source sample, only surviving sampled identities are a
413
+ // proven lower bound. Subtracting sampled retirements from the source's
414
+ // lower bound could overstate active work if unseen rows are retired too.
415
+ return { kind: 'lower_bound', value: activeSampleCount };
416
+ }
417
+ return { kind: 'exact', value: activeSampleCount };
418
+ };
419
+ const adjustAge = (
420
+ bucket: 'blocked' | 'deadLettered',
421
+ rows: readonly QueueRetirementCandidate[],
422
+ depth: Measured<number>,
423
+ ): Measured<number> => {
424
+ const measured = observation.oldestAgeMs[bucket];
425
+ if (measured.kind !== 'exact' && measured.kind !== 'lower_bound') {
426
+ return measured;
427
+ }
428
+ if (depth.kind === 'unavailable') {
429
+ return {
430
+ kind: 'unavailable',
431
+ reason: `retirement candidate coverage is incomplete for ${bucket}`,
432
+ };
433
+ }
434
+ const remaining = rows.filter((row) => !retiredItemIds.has(row.itemId));
435
+ if (remaining.length > 0) {
436
+ const value = Math.max(...remaining.map((row) => row.ageMs));
437
+ return measured.kind === 'lower_bound' ||
438
+ truncatedBuckets.has(bucket) ||
439
+ depth.kind === 'lower_bound'
440
+ ? { kind: 'lower_bound', value }
441
+ : { kind: 'exact', value };
442
+ }
443
+ if (
444
+ measured.kind === 'exact' &&
445
+ depth.kind === 'exact' &&
446
+ depth.value === 0
447
+ ) {
448
+ return { kind: 'not_applicable', reason: 'no work in this bucket' };
449
+ }
450
+ if (
451
+ measured.kind === 'lower_bound' ||
452
+ truncatedBuckets.has(bucket) ||
453
+ depth.kind === 'lower_bound'
454
+ ) {
455
+ // The bounded source page may have unseen, non-retired rows.
456
+ return { kind: 'lower_bound', value: 0 };
457
+ }
458
+ return measured;
459
+ };
460
+ const blockedDepth = adjustDepth('blocked', blocked);
461
+ const deadLetteredDepth = adjustDepth('deadLettered', deadLettered);
462
+ return {
463
+ ...observation,
464
+ depth: {
465
+ ...observation.depth,
466
+ blocked: blockedDepth,
467
+ deadLettered: deadLetteredDepth,
468
+ },
469
+ oldestAgeMs: {
470
+ ...observation.oldestAgeMs,
471
+ blocked: adjustAge('blocked', blocked, blockedDepth),
472
+ deadLettered: adjustAge('deadLettered', deadLettered, deadLetteredDepth),
473
+ },
474
+ };
475
+ }
476
+
366
477
  function combineMeasurements(
367
478
  observations: readonly QueueObservation[],
368
479
  select: (observation: QueueObservation) => Measured<number>,
@@ -321,14 +321,23 @@ function rowMatchesCorrelation(
321
321
  correlationField: string,
322
322
  correlationValue: string,
323
323
  ): boolean {
324
- return resultCustomFieldRecords(row).some(
325
- (fields) => fields[correlationField] === correlationValue,
326
- );
324
+ const matches = resultCustomFieldRecords(row).filter(
325
+ (fields) =>
326
+ fields[correlationField] === correlationValue ||
327
+ (fields.name === correlationField && fields.value === correlationValue),
328
+ ).length;
329
+ if (matches > 1) {
330
+ throw new Error(
331
+ 'BetterContact scoped result has ambiguous correlation identity.',
332
+ );
333
+ }
334
+ return matches === 1;
327
335
  }
328
336
 
329
337
  function withoutCorrelationSelector(
330
338
  row: unknown,
331
339
  correlationField: string,
340
+ correlationValue: string,
332
341
  ): unknown {
333
342
  if (!row || typeof row !== 'object' || Array.isArray(row)) {
334
343
  return row;
@@ -337,7 +346,9 @@ function withoutCorrelationSelector(
337
346
  const customFields = record.custom_fields;
338
347
  const cleanRecord = (fields: Record<string, unknown>) => {
339
348
  const cleaned = { ...fields };
340
- delete cleaned[correlationField];
349
+ if (cleaned[correlationField] === correlationValue) {
350
+ delete cleaned[correlationField];
351
+ }
341
352
  return cleaned;
342
353
  };
343
354
  return {
@@ -345,6 +356,11 @@ function withoutCorrelationSelector(
345
356
  custom_fields: Array.isArray(customFields)
346
357
  ? customFields
347
358
  .filter(isPlainRecord)
359
+ .filter(
360
+ (fields) =>
361
+ fields.name !== correlationField ||
362
+ fields.value !== correlationValue,
363
+ )
348
364
  .map(cleanRecord)
349
365
  .filter((fields) => Object.keys(fields).length > 0)
350
366
  : isPlainRecord(customFields)
@@ -381,7 +397,13 @@ export function isolateBetterContactScopedResult(
381
397
  }
382
398
  return {
383
399
  ...scopedResult,
384
- data: [withoutCorrelationSelector(matches[0], scope.correlationField)],
400
+ data: [
401
+ withoutCorrelationSelector(
402
+ matches[0],
403
+ scope.correlationField,
404
+ scope.correlationValue,
405
+ ),
406
+ ],
385
407
  };
386
408
  }
387
409
 
@@ -417,11 +439,15 @@ function withoutCorrelationField(
417
439
  if (!row || typeof row !== 'object' || Array.isArray(row)) {
418
440
  return row;
419
441
  }
420
- const correlationField = correlationEntryForItem(item)?.[0];
421
- if (!correlationField) {
442
+ const correlationEntry = correlationEntryForItem(item);
443
+ if (!correlationEntry) {
422
444
  return row;
423
445
  }
424
- return withoutCorrelationSelector(row, correlationField);
446
+ return withoutCorrelationSelector(
447
+ row,
448
+ correlationEntry[0],
449
+ String(correlationEntry[1]),
450
+ );
425
451
  }
426
452
 
427
453
  function scopedRequestIdForItem(