deepline 0.3.113 → 0.3.115

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 (50) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +3 -1
  2. package/dist/bundling-sources/sdk/src/compat.ts +167 -11
  3. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  4. package/dist/bundling-sources/sdk/src/skills-version.ts +27 -1
  5. package/dist/bundling-sources/shared_libs/monitors/inbound-queue.ts +6 -2
  6. package/dist/bundling-sources/shared_libs/observability/dlq.ts +942 -0
  7. package/dist/bundling-sources/shared_libs/observability/queue-health.ts +361 -6
  8. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +24 -24
  9. package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +2 -0
  10. package/dist/bundling-sources/shared_libs/observability/scheduled-work.ts +29 -1
  11. package/dist/bundling-sources/shared_libs/play-runtime/admission-transport-policy.ts +91 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +2 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/direct-admission-deadline.ts +34 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/internal-bootstrap-token.ts +325 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/play-admission-outcome.ts +134 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/contracts.ts +13 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/index.ts +4 -1
  19. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/postgres-adapter.ts +13 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/play-webhook-admission-probe.ts +32 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/play-webhook-identity.ts +28 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +7 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-cron-fire-task.ts +100 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +313 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +111 -3
  26. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +16 -0
  27. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +67 -8
  28. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +1734 -147
  29. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +74 -13
  30. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-queue-health.ts +245 -81
  31. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/submit-admission.ts +79 -15
  32. package/dist/bundling-sources/shared_libs/play-runtime/webhook-receipt-retry.ts +21 -21
  33. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +4 -1
  34. package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +64 -0
  35. package/dist/bundling-sources/shared_libs/product-notifications/default-policy.ts +1 -1
  36. package/dist/bundling-sources/shared_libs/product-notifications/events.ts +20 -1
  37. package/dist/bundling-sources/shared_libs/security/receipt-admission-token.ts +224 -0
  38. package/dist/cli/index.js +574 -114
  39. package/dist/cli/index.mjs +592 -133
  40. package/dist/index.d.mts +54 -1
  41. package/dist/index.d.ts +54 -1
  42. package/dist/index.js +8 -3
  43. package/dist/index.mjs +8 -3
  44. package/dist/install-integrity.json +8 -0
  45. package/dist/plays/bundle-play-file.mjs +1 -1
  46. package/dist/release.d.mts +1 -1
  47. package/dist/release.d.ts +1 -1
  48. package/dist/release.js +1 -1
  49. package/dist/release.mjs +1 -1
  50. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  /** Scheduler-neutral queue/workset health contract. */
2
2
 
3
3
  import { measuredValue, type Measured } from './scheduled-work-v2';
4
+ import type { Telemetry, TelemetryFields } from './telemetry';
4
5
  export type { Measured } from './scheduled-work-v2';
5
6
  export {
6
7
  exact,
@@ -117,6 +118,15 @@ export const OPERATIONS_QUEUE_CATALOG = [
117
118
  60_000,
118
119
  180_000,
119
120
  ),
121
+ queue(
122
+ 'compute-billing-jobs',
123
+ 'Compute billing jobs',
124
+ 'leased_workset',
125
+ 'billing',
126
+ 'Settle durable compute billing obligations after a runtime run ends',
127
+ 300_000,
128
+ 900_000,
129
+ ),
120
130
  queue(
121
131
  'play-webhook-receipts',
122
132
  'Play webhook receipts',
@@ -448,6 +458,7 @@ export type QueueHealth = {
448
458
 
449
459
  export const QUEUE_HEALTH_TELEMETRY_EVENT = 'operations.queue.health' as const;
450
460
  export const QUEUE_HEALTH_TELEMETRY_SCHEMA_VERSION = 1 as const;
461
+ const UNOBSERVED_SIGNAL_REPEAT_MS = 5 * 60_000;
451
462
 
452
463
  export type QueueHealthSignal = {
453
464
  readonly schemaVersion: typeof QUEUE_HEALTH_TELEMETRY_SCHEMA_VERSION;
@@ -478,9 +489,10 @@ export type QueueHealthSignal = {
478
489
  * Project a durable observation into the one cross-runtime queue signal.
479
490
  * Healthy steady state is deliberately silent. Actionable backlog problems
480
491
  * repeat on every observation so a log-based monitor cannot resolve while the
481
- * queue remains unhealthy; persistent adapter absence is emitted once because
482
- * the dashboard already keeps that configuration state visible. The first
483
- * healthy observation after a problem emits recovery.
492
+ * queue remains unhealthy. Persistent adapter absence is repeated on a
493
+ * five-minute boundary so a log-backed dashboard cannot go blank while an
494
+ * adapter remains absent. The first healthy observation after a problem emits
495
+ * recovery.
484
496
  */
485
497
  export function deriveQueueHealthSignal(input: {
486
498
  definition: QueueCatalogEntry;
@@ -500,14 +512,19 @@ export function deriveQueueHealthSignal(input: {
500
512
  previousHealth.status !== 'healthy';
501
513
  if (health.status === 'healthy' && !recovering) return null;
502
514
  // A permanently unavailable adapter is a catalog/configuration finding, not
503
- // a one-minute incident. Emit it once; the dashboard keeps the state visible
504
- // and a later observation with real evidence emits recovery.
515
+ // a one-minute incident. Repeat it only when the five-minute bucket changes;
516
+ // this keeps the state visible without recreating one log per observer tick.
505
517
  if (
506
518
  health.status === 'unobserved' &&
507
519
  previousHealth?.status === 'unobserved' &&
508
520
  previousHealth.reason === health.reason
509
521
  ) {
510
- return null;
522
+ const previousBucket = Math.floor(
523
+ (input.previousObservation?.observedAt ?? 0) /
524
+ UNOBSERVED_SIGNAL_REPEAT_MS,
525
+ );
526
+ const currentBucket = Math.floor(now / UNOBSERVED_SIGNAL_REPEAT_MS);
527
+ if (previousBucket === currentBucket) return null;
511
528
  }
512
529
  return {
513
530
  schemaVersion: QUEUE_HEALTH_TELEMETRY_SCHEMA_VERSION,
@@ -676,3 +693,341 @@ export function deriveQueueHealth(
676
693
  ...(worstAgeMs === undefined ? {} : { worstAgeMs, worstAgeRatio: ratio }),
677
694
  };
678
695
  }
696
+
697
+ // Legacy v1 snapshot helpers remain exported for the webhook/cron collectors
698
+ // during the v1 -> v2 observability rollout. They are intentionally isolated
699
+ // from the scheduler-neutral v2 contract above.
700
+ export const QUEUE_HEALTH_SCHEMA_VERSION = 1 as const;
701
+ export type QueueHealthAvailability = 'available' | 'unavailable';
702
+ export type QueueHealthFreshness = 'fresh' | 'stale';
703
+ export type QueueHealthLayer =
704
+ | 'admission'
705
+ | 'delivery'
706
+ | 'execution'
707
+ | 'projection'
708
+ | 'reconciliation'
709
+ | 'other';
710
+ export type QueueHealthAttentionReason =
711
+ | 'source_unavailable'
712
+ | 'snapshot_stale'
713
+ | 'blocked_work'
714
+ | 'dead_letter_work'
715
+ | 'backlog_aged'
716
+ | 'backlog_count_inexact'
717
+ | 'backlog_age_unavailable';
718
+ export type QueueHealthCount = { value: number | null; exact: boolean };
719
+ export type QueueHealthCapacity = {
720
+ active: number | null;
721
+ limit: number | null;
722
+ };
723
+ export type QueueHealthSnapshot = {
724
+ schemaVersion: typeof QUEUE_HEALTH_SCHEMA_VERSION;
725
+ queueId: string;
726
+ owner: string;
727
+ layer: QueueHealthLayer;
728
+ transport: string;
729
+ observedAt: number;
730
+ freshness: { staleAfterMs: number };
731
+ availability: QueueHealthAvailability;
732
+ queued: QueueHealthCount;
733
+ running: QueueHealthCount;
734
+ retryWaiting: QueueHealthCount;
735
+ blocked: QueueHealthCount;
736
+ deadLetter: QueueHealthCount;
737
+ oldestQueuedAgeMs: number | null;
738
+ oldestBlockedAgeMs: number | null;
739
+ capacity: QueueHealthCapacity;
740
+ thresholds: { degradedOldestQueuedAgeMs: number };
741
+ };
742
+ export type QueueHealthSnapshotInput = Omit<
743
+ QueueHealthSnapshot,
744
+ 'schemaVersion'
745
+ >;
746
+ type LegacyQueueHealthStatus = 'healthy' | 'degraded' | 'blocked' | 'unknown';
747
+ type LegacyQueueHealthAssessment = {
748
+ status: LegacyQueueHealthStatus;
749
+ freshness: QueueHealthFreshness;
750
+ observedAgeMs: number;
751
+ attentionReasons: readonly QueueHealthAttentionReason[];
752
+ };
753
+ export type QueueHealthAssessment = LegacyQueueHealthAssessment;
754
+
755
+ const legacyLayers = new Set<QueueHealthLayer>([
756
+ 'admission',
757
+ 'delivery',
758
+ 'execution',
759
+ 'projection',
760
+ 'reconciliation',
761
+ 'other',
762
+ ]);
763
+ const legacyCountNames = [
764
+ 'queued',
765
+ 'running',
766
+ 'retryWaiting',
767
+ 'blocked',
768
+ 'deadLetter',
769
+ ] as const;
770
+
771
+ export function parseQueueHealthSnapshot(input: unknown): QueueHealthSnapshot {
772
+ const record = legacyRecord(input, 'Queue health snapshot');
773
+ if (record.schemaVersion !== 1)
774
+ throw new Error('Queue health schemaVersion must be 1.');
775
+ const layer = legacyString(record.layer, 'layer') as QueueHealthLayer;
776
+ if (!legacyLayers.has(layer))
777
+ throw new Error('Queue health layer is not supported.');
778
+ const availability = legacyString(
779
+ record.availability,
780
+ 'availability',
781
+ ) as QueueHealthAvailability;
782
+ if (availability !== 'available' && availability !== 'unavailable')
783
+ throw new Error(
784
+ 'Queue health availability must be available or unavailable.',
785
+ );
786
+ const freshness = legacyRecord(record.freshness, 'freshness');
787
+ const capacity = legacyRecord(record.capacity, 'capacity');
788
+ const thresholds = legacyRecord(record.thresholds, 'thresholds');
789
+ const snapshot: QueueHealthSnapshot = {
790
+ schemaVersion: 1,
791
+ queueId: legacyIdentifier(record.queueId, 'queueId'),
792
+ owner: legacyIdentifier(record.owner, 'owner'),
793
+ layer,
794
+ transport: legacyIdentifier(record.transport, 'transport'),
795
+ observedAt: legacyNumber(record.observedAt, 'observedAt'),
796
+ freshness: {
797
+ staleAfterMs: legacyPositive(
798
+ freshness.staleAfterMs,
799
+ 'freshness.staleAfterMs',
800
+ ),
801
+ },
802
+ availability,
803
+ queued: legacyCount(record.queued, 'queued'),
804
+ running: legacyCount(record.running, 'running'),
805
+ retryWaiting: legacyCount(record.retryWaiting, 'retryWaiting'),
806
+ blocked: legacyCount(record.blocked, 'blocked'),
807
+ deadLetter: legacyCount(record.deadLetter, 'deadLetter'),
808
+ oldestQueuedAgeMs: legacyNullableNumber(
809
+ record.oldestQueuedAgeMs,
810
+ 'oldestQueuedAgeMs',
811
+ ),
812
+ oldestBlockedAgeMs: legacyNullableNumber(
813
+ record.oldestBlockedAgeMs,
814
+ 'oldestBlockedAgeMs',
815
+ ),
816
+ capacity: {
817
+ active: legacyNullableNumber(capacity.active, 'capacity.active'),
818
+ limit: legacyNullableNumber(capacity.limit, 'capacity.limit'),
819
+ },
820
+ thresholds: {
821
+ degradedOldestQueuedAgeMs: legacyPositive(
822
+ thresholds.degradedOldestQueuedAgeMs,
823
+ 'thresholds.degradedOldestQueuedAgeMs',
824
+ ),
825
+ },
826
+ };
827
+ if (snapshot.availability === 'unavailable') {
828
+ for (const name of legacyCountNames) {
829
+ const count = snapshot[name];
830
+ if (count.value !== null || count.exact)
831
+ throw new Error(
832
+ `Queue health ${name} must be unknown when availability is unavailable.`,
833
+ );
834
+ }
835
+ if (
836
+ snapshot.oldestQueuedAgeMs !== null ||
837
+ snapshot.oldestBlockedAgeMs !== null
838
+ )
839
+ throw new Error(
840
+ 'Queue health oldestQueuedAgeMs must be null when availability is unavailable.',
841
+ );
842
+ }
843
+ if (
844
+ snapshot.queued.exact &&
845
+ snapshot.queued.value === 0 &&
846
+ snapshot.retryWaiting.exact &&
847
+ snapshot.retryWaiting.value === 0 &&
848
+ snapshot.oldestQueuedAgeMs !== null
849
+ ) {
850
+ throw new Error(
851
+ 'Queue health oldestQueuedAgeMs requires queued or retry-waiting work.',
852
+ );
853
+ }
854
+ if (
855
+ snapshot.blocked.exact &&
856
+ snapshot.blocked.value === 0 &&
857
+ snapshot.deadLetter.exact &&
858
+ snapshot.deadLetter.value === 0 &&
859
+ snapshot.oldestBlockedAgeMs !== null
860
+ ) {
861
+ throw new Error(
862
+ 'Queue health oldestBlockedAgeMs requires blocked or dead-letter work.',
863
+ );
864
+ }
865
+ return snapshot;
866
+ }
867
+
868
+ export function createQueueHealthSnapshot(
869
+ input: QueueHealthSnapshotInput,
870
+ ): QueueHealthSnapshot {
871
+ return parseQueueHealthSnapshot({ ...input, schemaVersion: 1 });
872
+ }
873
+
874
+ export function assessQueueHealth(
875
+ input: QueueHealthSnapshot,
876
+ now = Date.now(),
877
+ ): LegacyQueueHealthAssessment {
878
+ const snapshot = parseQueueHealthSnapshot(input);
879
+ const observedAgeMs = Math.max(0, now - snapshot.observedAt);
880
+ const freshness: QueueHealthFreshness =
881
+ observedAgeMs > snapshot.freshness.staleAfterMs ? 'stale' : 'fresh';
882
+ if (snapshot.availability === 'unavailable')
883
+ return {
884
+ status: 'unknown',
885
+ freshness,
886
+ observedAgeMs,
887
+ attentionReasons: ['source_unavailable'],
888
+ };
889
+ if (freshness === 'stale')
890
+ return {
891
+ status: 'unknown',
892
+ freshness,
893
+ observedAgeMs,
894
+ attentionReasons: ['snapshot_stale'],
895
+ };
896
+ const attentionReasons: QueueHealthAttentionReason[] = [];
897
+ if (knownItems(snapshot.blocked)) attentionReasons.push('blocked_work');
898
+ if (knownItems(snapshot.deadLetter))
899
+ attentionReasons.push('dead_letter_work');
900
+ if (attentionReasons.length)
901
+ return { status: 'blocked', freshness, observedAgeMs, attentionReasons };
902
+ if (
903
+ snapshot.oldestQueuedAgeMs !== null &&
904
+ snapshot.oldestQueuedAgeMs >= snapshot.thresholds.degradedOldestQueuedAgeMs
905
+ )
906
+ return {
907
+ status: 'degraded',
908
+ freshness,
909
+ observedAgeMs,
910
+ attentionReasons: ['backlog_aged'],
911
+ };
912
+ if (
913
+ !snapshot.queued.exact ||
914
+ !snapshot.retryWaiting.exact ||
915
+ !snapshot.blocked.exact ||
916
+ !snapshot.deadLetter.exact
917
+ )
918
+ return {
919
+ status: 'unknown',
920
+ freshness,
921
+ observedAgeMs,
922
+ attentionReasons: ['backlog_count_inexact'],
923
+ };
924
+ if (
925
+ (knownItems(snapshot.queued) || knownItems(snapshot.retryWaiting)) &&
926
+ snapshot.oldestQueuedAgeMs === null
927
+ )
928
+ return {
929
+ status: 'unknown',
930
+ freshness,
931
+ observedAgeMs,
932
+ attentionReasons: ['backlog_age_unavailable'],
933
+ };
934
+ return { status: 'healthy', freshness, observedAgeMs, attentionReasons: [] };
935
+ }
936
+
937
+ export function queueHealthTelemetryFields(
938
+ input: QueueHealthSnapshot,
939
+ assessment = assessQueueHealth(input),
940
+ ): TelemetryFields {
941
+ const snapshot = parseQueueHealthSnapshot(input);
942
+ return {
943
+ queueHealthSchemaVersion: snapshot.schemaVersion,
944
+ queueId: snapshot.queueId,
945
+ queueOwner: snapshot.owner,
946
+ queueLayer: snapshot.layer,
947
+ queueTransport: snapshot.transport,
948
+ queueAvailability: snapshot.availability,
949
+ queueObservedAt: snapshot.observedAt,
950
+ queueFreshnessStaleAfterMs: snapshot.freshness.staleAfterMs,
951
+ queueFreshness: assessment.freshness,
952
+ queueHealth: assessment.status,
953
+ queueHealthObservedAgeMs: assessment.observedAgeMs,
954
+ queueQueued: snapshot.queued.value,
955
+ queueQueuedExact: snapshot.queued.exact,
956
+ queueRunning: snapshot.running.value,
957
+ queueRunningExact: snapshot.running.exact,
958
+ queueRetryWaiting: snapshot.retryWaiting.value,
959
+ queueRetryWaitingExact: snapshot.retryWaiting.exact,
960
+ queueBlocked: snapshot.blocked.value,
961
+ queueBlockedExact: snapshot.blocked.exact,
962
+ queueDeadLetter: snapshot.deadLetter.value,
963
+ queueDeadLetterExact: snapshot.deadLetter.exact,
964
+ queueOldestQueuedAgeMs: snapshot.oldestQueuedAgeMs,
965
+ queueOldestBlockedAgeMs: snapshot.oldestBlockedAgeMs,
966
+ queueCapacityActive: snapshot.capacity.active,
967
+ queueCapacityLimit: snapshot.capacity.limit,
968
+ queueDegradedOldestQueuedAgeMs:
969
+ snapshot.thresholds.degradedOldestQueuedAgeMs,
970
+ queueAttentionReasons: assessment.attentionReasons,
971
+ };
972
+ }
973
+
974
+ export async function emitQueueHealthSnapshot(input: {
975
+ telemetry: Pick<Telemetry, 'info' | 'warn'>;
976
+ snapshot: QueueHealthSnapshot;
977
+ now?: number;
978
+ }): Promise<LegacyQueueHealthAssessment> {
979
+ const assessment = assessQueueHealth(input.snapshot, input.now);
980
+ const fields = queueHealthTelemetryFields(input.snapshot, assessment);
981
+ await input.telemetry.info('queue.health.snapshot', fields);
982
+ if (assessment.status !== 'healthy')
983
+ await input.telemetry.warn('queue.health.attention', fields);
984
+ return assessment;
985
+ }
986
+
987
+ function legacyRecord(value: unknown, label: string): Record<string, unknown> {
988
+ if (!value || typeof value !== 'object' || Array.isArray(value))
989
+ throw new Error(`${label} must be an object.`);
990
+ return value as Record<string, unknown>;
991
+ }
992
+ function legacyString(value: unknown, label: string): string {
993
+ if (typeof value !== 'string' || !value.trim())
994
+ throw new Error(`Queue health ${label} must be a non-empty string.`);
995
+ return value.trim();
996
+ }
997
+ function legacyIdentifier(value: unknown, label: string): string {
998
+ const result = legacyString(value, label);
999
+ if (result.length > 96 || !/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(result))
1000
+ throw new Error(
1001
+ `Queue health ${label} must be a stable low-cardinality identifier.`,
1002
+ );
1003
+ return result;
1004
+ }
1005
+ function legacyNumber(value: unknown, label: string): number {
1006
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
1007
+ throw new Error(
1008
+ `Queue health ${label} must be a finite non-negative number.`,
1009
+ );
1010
+ return value;
1011
+ }
1012
+ function legacyPositive(value: unknown, label: string): number {
1013
+ const result = legacyNumber(value, label);
1014
+ if (result === 0) throw new Error(`Queue health ${label} must be positive.`);
1015
+ return result;
1016
+ }
1017
+ function legacyNullableNumber(value: unknown, label: string): number | null {
1018
+ return value === null ? null : legacyNumber(value, label);
1019
+ }
1020
+ function legacyCount(value: unknown, label: string): QueueHealthCount {
1021
+ const record = legacyRecord(value, label);
1022
+ if (typeof record.exact !== 'boolean')
1023
+ throw new Error(`Queue health ${label}.exact must be a boolean.`);
1024
+ const count = legacyNullableNumber(record.value, `${label}.value`);
1025
+ if (count === null && record.exact)
1026
+ throw new Error(
1027
+ `Queue health ${label}.exact cannot be true when value is unknown.`,
1028
+ );
1029
+ return { value: count, exact: record.exact };
1030
+ }
1031
+ function knownItems(count: QueueHealthCount): boolean {
1032
+ return count.value !== null && count.value > 0;
1033
+ }
@@ -976,13 +976,13 @@
976
976
  "domainReportRequired": true
977
977
  },
978
978
  {
979
- "id": "vercel.play-webhook-receipt-recovery",
979
+ "id": "convex.recover-play-webhook-receipts",
980
980
  "name": "Recover Play webhook receipts",
981
- "scheduler": "vercel_cron",
982
- "cadence": "* * * * *",
981
+ "scheduler": "convex_cron",
982
+ "cadence": "every 60s",
983
983
  "timezone": "UTC",
984
- "source": "apps/deepline-api/vercel.json",
985
- "locator": "/api/v2/cron/play-webhook-receipt-recovery",
984
+ "source": "packages/convex/crons.ts",
985
+ "locator": "recover Play webhook receipts",
986
986
  "owner": "runtime",
987
987
  "criticality": "critical",
988
988
  "purpose": "Recover Play webhook receipts whose admission/dispatch continuation is delayed or stranded.",
@@ -990,7 +990,25 @@
990
990
  "failureDefinition": "A receipt cannot be safely recovered, remains stranded, or evidence cannot persist.",
991
991
  "startGraceMs": 300000,
992
992
  "runTimeoutMs": 1800000,
993
- "failureAlert": "vercel_route_wrapper",
993
+ "failureAlert": "convex_action_wrapper",
994
+ "domainReportRequired": true
995
+ },
996
+ {
997
+ "id": "convex.recover-signal-radar-inbound-receipts",
998
+ "name": "Recover Signal Radar inbound receipts",
999
+ "scheduler": "convex_cron",
1000
+ "cadence": "every 60s",
1001
+ "timezone": "UTC",
1002
+ "source": "packages/convex/crons.ts",
1003
+ "locator": "recover Signal Radar inbound receipts",
1004
+ "owner": "monitors",
1005
+ "criticality": "critical",
1006
+ "purpose": "Recover Signal Radar inbound receipts whose processing is delayed or stranded.",
1007
+ "successDefinition": "Every eligible receipt is retried or terminally classified with evidence; no eligible receipts is a valid no-op.",
1008
+ "failureDefinition": "A receipt cannot be safely recovered, remains stranded, or evidence cannot persist.",
1009
+ "startGraceMs": 300000,
1010
+ "runTimeoutMs": 1800000,
1011
+ "failureAlert": "convex_action_wrapper",
994
1012
  "domainReportRequired": true
995
1013
  },
996
1014
  {
@@ -1011,24 +1029,6 @@
1011
1029
  "failureAlert": "convex_action_wrapper",
1012
1030
  "domainReportRequired": true
1013
1031
  },
1014
- {
1015
- "id": "github.modal-ci-runner-weekly-proof",
1016
- "name": "Modal CI runner",
1017
- "scheduler": "github_actions",
1018
- "cadence": "23 6 * * 1",
1019
- "timezone": "UTC",
1020
- "source": ".github/workflows/modal-ci-runner.yml",
1021
- "locator": "schedule:",
1022
- "owner": "platform",
1023
- "criticality": "maintenance",
1024
- "purpose": "Lint, test, deploy, and prove the pinned Modal CI runner image and isolated smoke path.",
1025
- "successDefinition": "Pinned package lint/tests, control-plane/image deploy, and exact-image credential-isolation smoke all pass.",
1026
- "failureDefinition": "Any lint/test/deploy/health/smoke/cleanup proof fails or image identity is wrong.",
1027
- "startGraceMs": 600000,
1028
- "runTimeoutMs": 3600000,
1029
- "failureAlert": "github_workflow_run",
1030
- "domainReportRequired": true
1031
- },
1032
1032
  {
1033
1033
  "id": "convex.async-usage-recovery-continuation",
1034
1034
  "name": "Async usage recovery continuation",
@@ -18,6 +18,8 @@ export const MANUALLY_RUNNABLE_CONVEX_CRON_JOB_IDS = [
18
18
  'convex.monitor-deployment-job-tick',
19
19
  'convex.provider-monitor-global-audit-daily',
20
20
  'convex.recover-notification-delivery-work',
21
+ 'convex.recover-play-webhook-receipts',
22
+ 'convex.recover-signal-radar-inbound-receipts',
21
23
  'convex.process-notification-usage-refreshes',
22
24
  'convex.observe-operational-queue-health',
23
25
  'convex.cleanup-expired-notification-audit-records',
@@ -1,4 +1,6 @@
1
- export const SCHEDULED_WORK_OBSERVABILITY_VERSION = 1 as const;
1
+ /** The only scheduled-work telemetry envelope emitted after the hard cut. */
2
+ export const SCHEDULED_WORK_OBSERVABILITY_VERSION = 2 as const;
3
+ export const SCHEDULED_WORK_CONTRACT_VERSION = 2 as const;
2
4
 
3
5
  export type ScheduledWorkScheduler =
4
6
  | 'convex_cron'
@@ -11,6 +13,17 @@ export type ScheduledWorkScheduler =
11
13
 
12
14
  export type ScheduledWorkOutcome = 'running' | 'succeeded' | 'failed';
13
15
  export type ScheduledWorkTrigger = 'scheduled' | 'manual';
16
+ export type ScheduledWorkDomainOutcome = 'success' | 'warning' | 'failure';
17
+ export type ScheduledWorkFailureClass = 'execution' | 'domain';
18
+
19
+ export type ScheduledWorkTelemetryDisposition = {
20
+ /** The occurrence slot this event belongs to, when one exists. */
21
+ occurrenceId?: string;
22
+ expectedAt?: number;
23
+ /** Explicit domain result; absence is a migration gap, not a green run. */
24
+ domainOutcome?: ScheduledWorkDomainOutcome;
25
+ failureClass?: ScheduledWorkFailureClass;
26
+ };
14
27
 
15
28
  export type ScheduledWorkVolume = {
16
29
  unit: string;
@@ -44,9 +57,12 @@ export type ScheduledWorkObservation = {
44
57
 
45
58
  export function scheduledWorkTelemetryFields(
46
59
  observation: ScheduledWorkObservation,
60
+ disposition: ScheduledWorkTelemetryDisposition = {},
47
61
  ): Record<string, string | number> {
48
62
  return {
49
63
  scheduledWorkSchemaVersion: SCHEDULED_WORK_OBSERVABILITY_VERSION,
64
+ // Keep one explicit marker for the hard-cut occurrence/domain contract.
65
+ scheduledWorkContractVersion: SCHEDULED_WORK_CONTRACT_VERSION,
50
66
  scheduledWorkJobId: observation.jobId,
51
67
  scheduledWorkJobName: observation.jobName,
52
68
  scheduledWorkScheduler: observation.scheduler,
@@ -55,6 +71,18 @@ export function scheduledWorkTelemetryFields(
55
71
  scheduledWorkOutcome: observation.outcome,
56
72
  scheduledWorkInvocationId: observation.invocationId,
57
73
  scheduledWorkExpectedWithinMs: observation.expectedWithinMs,
74
+ ...(disposition.occurrenceId === undefined
75
+ ? {}
76
+ : { scheduledWorkOccurrenceId: disposition.occurrenceId }),
77
+ ...(disposition.expectedAt === undefined
78
+ ? {}
79
+ : { scheduledWorkExpectedAt: disposition.expectedAt }),
80
+ ...(disposition.domainOutcome === undefined
81
+ ? {}
82
+ : { scheduledWorkDomainOutcome: disposition.domainOutcome }),
83
+ ...(disposition.failureClass === undefined
84
+ ? {}
85
+ : { scheduledWorkFailureClass: disposition.failureClass }),
58
86
  ...(observation.trigger === undefined
59
87
  ? {}
60
88
  : { scheduledWorkTrigger: observation.trigger }),
@@ -0,0 +1,91 @@
1
+ import { PLAY_WEBHOOK_RECEIPT_LARGE_BATCH_MAX_RECEIPTS } from '../security/receipt-admission-token';
2
+
3
+ /**
4
+ * The per-request concurrency budget for moving a durable Play source into
5
+ * the Runtime Postgres + Absurd admission transaction. This is not an
6
+ * execution limit: Absurd owns execution capacity independently.
7
+ *
8
+ * Thirty-two is the private batch's entire scheduler-client budget for a
9
+ * normal 1,000-delivery burst. One client remains reserved below, rather than
10
+ * letting source admission consume every control-plane connection.
11
+ */
12
+ export const PLAY_ADMISSION_MAX_PARALLELISM = 32;
13
+
14
+ /**
15
+ * The private batch owns all 32 serverless scheduler clients, but one remains
16
+ * available for executor-token and other critical runtime control-plane work.
17
+ * Only atomic Runtime Postgres + Absurd submission uses this value.
18
+ * Source-lifecycle preflight has its own bounded budget because it does not
19
+ * consume a scheduler-pool client.
20
+ */
21
+ export const PLAY_ADMISSION_MAX_ACTIVE_SUBMITS =
22
+ PLAY_ADMISSION_MAX_PARALLELISM - 1;
23
+
24
+ /**
25
+ * A batch is allowed to prepare more independent source lifecycles than it
26
+ * may submit to Runtime Postgres at once. Preparation is mostly Convex/CPU
27
+ * work (read the immutable receipt, validate its pinned launch, mint its
28
+ * authority); it must not sit behind a scarce PgBouncer connection. The
29
+ * scheduler submitter remains capped by `PLAY_ADMISSION_MAX_ACTIVE_SUBMITS`.
30
+ *
31
+ * This is deliberately finite. It lets a 1K burst fill the 31 durable-submit
32
+ * slots while prior sources are still doing their non-Postgres preparation,
33
+ * without turning the private route into an unbounded fan-out.
34
+ */
35
+ export const PLAY_ADMISSION_MAX_ACTIVE_LIFECYCLES = 256;
36
+
37
+ /**
38
+ * A private 1K batch may queue behind the bounded scheduler submit cohort for
39
+ * longer than an ordinary single-run request, but never beyond this deadline.
40
+ */
41
+ export const PLAY_ADMISSION_MAX_ACQUIRE_QUEUE_WAIT_MS = 120_000;
42
+
43
+ /**
44
+ * One managed transport request may carry this many independently durable
45
+ * sources. The Vercel adapter processes one legacy wave at bounded
46
+ * source-lifecycle concurrency; its scheduler submit gate separately reserves
47
+ * critical control-plane capacity.
48
+ */
49
+ export const PLAY_ADMISSION_BATCH_SIZE = PLAY_ADMISSION_MAX_PARALLELISM;
50
+
51
+ /**
52
+ * `batch_v2` carries one full, bounded ingress burst into a single Vercel
53
+ * invocation. The invocation still submits at most
54
+ * `PLAY_ADMISSION_MAX_ACTIVE_SUBMITS` Runs at a time; this only removes the
55
+ * serial Convex-to-Vercel round trip between 32-receipt waves. The current
56
+ * dispatcher sends one request and lets that request drain its owned tail.
57
+ */
58
+ export const PLAY_ADMISSION_LARGE_BATCH_SIZE =
59
+ PLAY_WEBHOOK_RECEIPT_LARGE_BATCH_MAX_RECEIPTS;
60
+
61
+ /**
62
+ * `batch_v1` remains during the Convex-before-Vercel rollout window. It
63
+ * drains this many 32-source waves from one fenced Workpool action. New
64
+ * Retained custom-runner v2 actions may take a second large wave while their
65
+ * finite rollout cohort drains. Current scheduled dispatches own one
66
+ * exact wave and do not use this compatibility limit.
67
+ */
68
+ export const PLAY_ADMISSION_BATCHES_PER_DISPATCH = 32;
69
+
70
+ export const PLAY_ADMISSION_LARGE_BATCHES_PER_DISPATCH = 2;
71
+
72
+ /**
73
+ * Upper bound for one direct drain under continuous traffic, not an admission
74
+ * SLO or a debounce. A request stops as soon as its queue is empty. Keep the
75
+ * exported name and persisted deadline compatible with retained dispatches.
76
+ */
77
+ export const PLAY_ADMISSION_DIRECT_BATCH_COLLECTION_MAX_MS = 60_000;
78
+
79
+ /** Poll/debounce interval for retained legacy dispatch implementations only. */
80
+ export const PLAY_ADMISSION_DIRECT_BATCH_QUIET_MS = 250;
81
+
82
+ /**
83
+ * Workpool is recovery, not the normal batch transport. It becomes eligible
84
+ * quickly, but the ownerless direct-dispatch fence keeps it from claiming the
85
+ * same source while the private request drains. That gives a lost action
86
+ * prompt recovery without adding another normal-path batch request.
87
+ */
88
+ export const PLAY_ADMISSION_DIRECT_FALLBACK_DELAY_MS = 15_000;
89
+
90
+ /** A pending source older than this is actionable queue delay. */
91
+ export const PLAY_ADMISSION_DEGRADED_QUEUE_AGE_MS = 60_000;
@@ -1123,6 +1123,8 @@ export function resolveToolRuntimeTimeoutMs(
1123
1123
  normalized === 'deeplineagent_deeplineagent' ||
1124
1124
  normalized === 'ai_inference' ||
1125
1125
  normalized === 'deeplineagent_ai_inference' ||
1126
+ normalized === 'ai_evaluate' ||
1127
+ normalized === 'deeplineagent_ai_evaluate' ||
1126
1128
  normalized === 'aiinference'
1127
1129
  ? DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS
1128
1130
  : DEFAULT_TOOL_RUNTIME_TIMEOUT_MS;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Bounds a synchronous direct-admission attempt. The source and its delayed
3
+ * Workpool recovery item already committed before this helper is used, so a
4
+ * timeout is deliberately an ambiguous outcome rather than a failed delivery.
5
+ * The race does not cancel already-started work; a replay must use the same
6
+ * idempotency identity and converge on the existing Runtime Run/Absurd task.
7
+ */
8
+ export const PLAY_DIRECT_ADMISSION_DEADLINE_MS = 1_500;
9
+
10
+ export class PlayDirectAdmissionDeadlineExceededError extends Error {
11
+ constructor(timeoutMs: number) {
12
+ super(`Direct Play admission exceeded its ${timeoutMs}ms deadline.`);
13
+ this.name = 'PlayDirectAdmissionDeadlineExceededError';
14
+ }
15
+ }
16
+
17
+ export async function withPlayDirectAdmissionDeadline<T>(
18
+ fn: () => Promise<T> | T,
19
+ timeoutMs = PLAY_DIRECT_ADMISSION_DEADLINE_MS,
20
+ ): Promise<T> {
21
+ let timer: ReturnType<typeof setTimeout> | undefined;
22
+ const work = Promise.resolve().then(fn);
23
+ const deadline = new Promise<never>((_resolve, reject) => {
24
+ timer = setTimeout(
25
+ () => reject(new PlayDirectAdmissionDeadlineExceededError(timeoutMs)),
26
+ timeoutMs,
27
+ );
28
+ });
29
+ try {
30
+ return await Promise.race([work, deadline]);
31
+ } finally {
32
+ if (timer !== undefined) clearTimeout(timer);
33
+ }
34
+ }