@absolutejs/voice 0.0.22-beta.383 → 0.0.22-beta.385

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.
@@ -4686,6 +4686,136 @@ var appendRealCallRecoveryActionQuery = (href, query) => {
4686
4686
  const search = new URLSearchParams(entries).toString();
4687
4687
  return `${base}${separator}${search}${hash ? `#${hash}` : ""}`;
4688
4688
  };
4689
+ var sleepVoiceRealCallProfileRecoveryLoop = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4690
+ var describeVoiceRealCallProfileRecoveryLoopAction = (action) => [
4691
+ action.label ?? action.id ?? "recovery action",
4692
+ action.profileId ? `profile=${action.profileId}` : undefined,
4693
+ action.href
4694
+ ].filter(Boolean).join(" ");
4695
+ var defaultVoiceRealCallProfileRecoveryLoopActionFilter = (action, readinessCheckLabel) => action.method?.toUpperCase() === "POST" && action.sourceCheckLabel === readinessCheckLabel && typeof action.href === "string" && action.href.length > 0;
4696
+ var uniqueVoiceRealCallProfileRecoveryLoopActions = (actions) => {
4697
+ const seen = new Set;
4698
+ return actions.filter((action) => {
4699
+ const key = `${action.method?.toUpperCase() ?? "GET"} ${action.href ?? ""}`;
4700
+ if (seen.has(key)) {
4701
+ return false;
4702
+ }
4703
+ seen.add(key);
4704
+ return true;
4705
+ });
4706
+ };
4707
+ var runVoiceRealCallProfileRecoveryLoop = async (options) => {
4708
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
4709
+ const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
4710
+ const jobPollMs = options.jobPollMs ?? 1200;
4711
+ const jobTimeoutMs = options.jobTimeoutMs ?? 600000;
4712
+ const readinessCheckLabel = options.readinessCheckLabel ?? "Real-call profile history";
4713
+ const fetchImpl = options.fetch ?? fetch;
4714
+ const recoveryActionsHref = options.recoveryActionsHref ?? "/api/production-readiness/recovery-actions";
4715
+ const readinessHref = options.readinessHref ?? "/api/production-readiness";
4716
+ const refreshHref = options.refreshHref === undefined ? "/api/voice/real-call-profile-history/refresh" : options.refreshHref;
4717
+ const jobHref = options.jobHref ?? "/api/voice/real-call-profile-history/actions";
4718
+ const toAbsoluteUrl = (href) => new URL(href, baseUrl).toString();
4719
+ const parseJson = async (response) => {
4720
+ const text = await response.text();
4721
+ try {
4722
+ return JSON.parse(text);
4723
+ } catch (error) {
4724
+ throw new Error(`Expected JSON from ${response.url}, got: ${text.slice(0, 300)}`, { cause: error });
4725
+ }
4726
+ };
4727
+ const fetchJson = async (href, init) => {
4728
+ const response = await fetchImpl(toAbsoluteUrl(href), {
4729
+ headers: { accept: "application/json", ...init?.headers },
4730
+ ...init,
4731
+ signal: init?.signal ?? AbortSignal.timeout(requestTimeoutMs)
4732
+ });
4733
+ if (!response.ok) {
4734
+ throw new Error(`${href} returned HTTP ${String(response.status)}.`);
4735
+ }
4736
+ return parseJson(response);
4737
+ };
4738
+ const resolveJobHref = (jobId) => typeof jobHref === "function" ? jobHref(jobId) : `${jobHref.replace(/\/$/, "")}/${jobId}`;
4739
+ const getGate = async (fresh = false) => {
4740
+ const href = fresh ? `${readinessHref}${readinessHref.includes("?") ? "&" : "?"}voiceRecoveryLoopFresh=${String(Date.now())}` : readinessHref;
4741
+ const readiness = await fetchJson(href);
4742
+ return readiness.checks?.find((check) => check.label === readinessCheckLabel) ?? null;
4743
+ };
4744
+ const actionsResponse = await fetchJson(recoveryActionsHref);
4745
+ const actionFilter = options.actionFilter ?? ((action) => defaultVoiceRealCallProfileRecoveryLoopActionFilter(action, readinessCheckLabel));
4746
+ const actions = uniqueVoiceRealCallProfileRecoveryLoopActions((actionsResponse.actions ?? []).filter(actionFilter));
4747
+ if (actions.length === 0) {
4748
+ const realCallProfileGate2 = await getGate();
4749
+ return {
4750
+ actionCount: 0,
4751
+ actions,
4752
+ jobs: [],
4753
+ ok: realCallProfileGate2?.status === "pass",
4754
+ realCallProfileGate: realCallProfileGate2,
4755
+ startFailures: []
4756
+ };
4757
+ }
4758
+ options.logger?.log(`Running ${String(actions.length)} real-call profile recovery action(s) in parallel.`);
4759
+ for (const action of actions) {
4760
+ options.logger?.log(`- ${describeVoiceRealCallProfileRecoveryLoopAction(action)}`);
4761
+ }
4762
+ const starts = await Promise.allSettled(actions.map(async (action) => {
4763
+ if (!action.href) {
4764
+ throw new Error("Recovery action is missing href.");
4765
+ }
4766
+ const body = await fetchJson(action.href, { method: "POST" });
4767
+ return { action, ...body };
4768
+ }));
4769
+ const startedJobs = starts.flatMap((result) => {
4770
+ if (result.status === "rejected") {
4771
+ return [];
4772
+ }
4773
+ return result.value.jobId ? [result.value] : [];
4774
+ });
4775
+ const startFailures = starts.flatMap((result, index) => result.status === "rejected" ? [
4776
+ {
4777
+ action: describeVoiceRealCallProfileRecoveryLoopAction(actions[index] ?? {}),
4778
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason)
4779
+ }
4780
+ ] : []);
4781
+ const pollJob = async (jobId) => {
4782
+ const deadline = Date.now() + jobTimeoutMs;
4783
+ while (Date.now() < deadline) {
4784
+ const body = await fetchJson(resolveJobHref(jobId));
4785
+ const job = body.job;
4786
+ if (!job) {
4787
+ throw new Error(`Recovery job ${jobId} was not found.`);
4788
+ }
4789
+ if (job.status === "pass" || job.status === "fail") {
4790
+ return job;
4791
+ }
4792
+ await sleepVoiceRealCallProfileRecoveryLoop(jobPollMs);
4793
+ }
4794
+ throw new Error(`Timed out waiting ${String(jobTimeoutMs)}ms for recovery job ${jobId}.`);
4795
+ };
4796
+ options.logger?.log(`Polling ${String(startedJobs.length)} recovery job(s) in parallel.`);
4797
+ const jobResults = await Promise.allSettled(startedJobs.map((start) => pollJob(start.jobId)));
4798
+ const jobs = jobResults.map((result, index) => ({
4799
+ action: describeVoiceRealCallProfileRecoveryLoopAction(startedJobs[index]?.action ?? {}),
4800
+ jobId: startedJobs[index]?.jobId,
4801
+ result: result.status === "fulfilled" ? result.value : {
4802
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason),
4803
+ status: "fail"
4804
+ }
4805
+ }));
4806
+ if (refreshHref !== false) {
4807
+ await fetchJson(refreshHref, { method: "POST" });
4808
+ }
4809
+ const realCallProfileGate = await getGate(true);
4810
+ return {
4811
+ actionCount: actions.length,
4812
+ actions,
4813
+ jobs,
4814
+ ok: startFailures.length === 0 && jobs.every((job) => job.result.status === "pass") && realCallProfileGate?.status === "pass",
4815
+ realCallProfileGate,
4816
+ startFailures
4817
+ };
4818
+ };
4689
4819
  var buildVoiceRealCallProfileRecoveryActions = (report, options = {}) => {
4690
4820
  const actions = [
4691
4821
  {
@@ -5021,21 +5151,22 @@ var buildVoiceRealCallProfileHistoryReport = (options = {}) => {
5021
5151
  ];
5022
5152
  const passingHistory = history.filter((report) => report.ok === true);
5023
5153
  const recommendationHistory = passingHistory.length > 0 ? passingHistory : history;
5024
- const profiles = buildVoiceProofTrendProfileSummaries(recommendationHistory, options);
5154
+ const profileHistory = history.length > 0 ? history : recommendationHistory;
5155
+ const profiles = buildVoiceProofTrendProfileSummaries(profileHistory, options);
5025
5156
  const summary = {
5026
- cycles: recommendationHistory.reduce((total, report) => total + (report.summary.cycles ?? report.cycles.length), 0),
5157
+ cycles: profileHistory.reduce((total, report) => total + (report.summary.cycles ?? report.cycles.length), 0),
5027
5158
  failedReports: history.filter((report) => report.ok !== true).length,
5028
- maxLiveP95Ms: maxNumber(recommendationHistory.map(readProofTrendMaxLiveP95)),
5029
- maxProviderP95Ms: maxNumber(recommendationHistory.map(readProofTrendMaxProviderP95)),
5030
- maxTurnP95Ms: maxNumber(recommendationHistory.map(readProofTrendMaxTurnP95)),
5159
+ maxLiveP95Ms: maxNumber(profileHistory.map(readProofTrendMaxLiveP95)),
5160
+ maxProviderP95Ms: maxNumber(profileHistory.map(readProofTrendMaxProviderP95)),
5161
+ maxTurnP95Ms: maxNumber(profileHistory.map(readProofTrendMaxTurnP95)),
5031
5162
  profileCount: profiles.length,
5032
5163
  profiles,
5033
- providers: readProofTrendProviders(recommendationHistory),
5034
- runtimeChannel: aggregateProofTrendRuntimeChannel(recommendationHistory.map(readProofTrendRuntimeChannel).filter((channel) => channel !== undefined))
5164
+ providers: readProofTrendProviders(profileHistory),
5165
+ runtimeChannel: aggregateProofTrendRuntimeChannel(profileHistory.map(readProofTrendRuntimeChannel).filter((channel) => channel !== undefined))
5035
5166
  };
5036
5167
  const trend = buildVoiceProofTrendReport({
5037
5168
  baseUrl: options.baseUrl,
5038
- cycles: flattenProofTrendCycles(recommendationHistory),
5169
+ cycles: flattenProofTrendCycles(profileHistory),
5039
5170
  generatedAt,
5040
5171
  maxAgeMs: options.maxAgeMs,
5041
5172
  now: options.now,
package/dist/index.d.ts CHANGED
@@ -30,12 +30,12 @@ export { assertVoicePlatformCoverage, buildVoicePlatformCoverageSummary, createV
30
30
  export { assertVoiceCompetitiveCoverage, buildVoiceCompetitiveCoverageReport, createVoiceCompetitiveCoverageRoutes, evaluateVoiceCompetitiveCoverage, renderVoiceCompetitiveCoverageHTML, renderVoiceCompetitiveCoverageMarkdown } from './competitiveCoverage';
31
31
  export type { VoiceCompetitiveCoverageAssertionInput, VoiceCompetitiveCoverageAssertionReport, VoiceCompetitiveCoverageIssue, VoiceCompetitiveCoverageLevel, VoiceCompetitiveCoverageReport, VoiceCompetitiveCoverageReportInput, VoiceCompetitiveCoverageRoutesOptions, VoiceCompetitiveCoverageStatus, VoiceCompetitiveCoverageSummary, VoiceCompetitiveDepthLevel, VoiceCompetitiveEvidence, VoiceCompetitiveSurface } from './competitiveCoverage';
32
32
  export type { VoicePlatformCoverageAssertionInput, VoicePlatformCoverageAssertionReport, VoicePlatformCoverageEvidence, VoicePlatformCoverageRoutesOptions, VoicePlatformCoverageStatus, VoicePlatformCoverageSummary, VoicePlatformCoverageSummaryInput, VoicePlatformCoverageSurface } from './platformCoverage';
33
- export { assertVoiceProofTrendEvidence, buildEmptyVoiceProofTrendReport, buildVoiceProofTrendProfileSummaries, buildVoiceProofTrendRecommendationReport, buildVoiceProofTrendReportFromRealCallProfiles, buildVoiceProofTrendReport, buildVoiceRealCallProfileEvidenceFromTraceEvents, buildVoiceRealCallProfileDefaults, buildVoiceRealCallProfileHistoryReport, buildVoiceRealCallProfileReadinessCheck, buildVoiceRealCallProfileRecoveryJobHistoryCheck, buildVoiceRealCallProfileRecoveryActions, createVoiceInMemoryRealCallProfileRecoveryJobStore, createVoiceRealCallProfileTraceCollector, createVoiceSQLiteRealCallProfileRecoveryJobStore, createVoiceProofTrendRecommendationRoutes, createVoiceProofTrendRoutes, createVoiceRealCallProfileHistoryRoutes, createVoiceRealCallProfileRecoveryActionRoutes, DEFAULT_VOICE_PROOF_TREND_PROFILE_DEFINITIONS, DEFAULT_VOICE_PROOF_TRENDS_MAX_AGE_MS, evaluateVoiceProofTrendEvidence, formatVoiceProofTrendAge, loadVoiceRealCallProfileEvidenceFromTraceStore, normalizeVoiceProofTrendReport, readVoiceProofTrendReportFile, renderVoiceProofTrendRecommendationHTML, renderVoiceProofTrendRecommendationMarkdown, renderVoiceRealCallProfileHistoryHTML, renderVoiceRealCallProfileHistoryMarkdown, resolveVoiceRealCallProfileProviderRoute } from './proofTrends';
33
+ export { assertVoiceProofTrendEvidence, buildEmptyVoiceProofTrendReport, buildVoiceProofTrendProfileSummaries, buildVoiceProofTrendRecommendationReport, buildVoiceProofTrendReportFromRealCallProfiles, buildVoiceProofTrendReport, buildVoiceRealCallProfileEvidenceFromTraceEvents, buildVoiceRealCallProfileDefaults, buildVoiceRealCallProfileHistoryReport, buildVoiceRealCallProfileReadinessCheck, buildVoiceRealCallProfileRecoveryJobHistoryCheck, buildVoiceRealCallProfileRecoveryActions, createVoiceInMemoryRealCallProfileRecoveryJobStore, createVoiceRealCallProfileTraceCollector, createVoiceSQLiteRealCallProfileRecoveryJobStore, createVoiceProofTrendRecommendationRoutes, createVoiceProofTrendRoutes, createVoiceRealCallProfileHistoryRoutes, createVoiceRealCallProfileRecoveryActionRoutes, DEFAULT_VOICE_PROOF_TREND_PROFILE_DEFINITIONS, DEFAULT_VOICE_PROOF_TRENDS_MAX_AGE_MS, evaluateVoiceProofTrendEvidence, formatVoiceProofTrendAge, loadVoiceRealCallProfileEvidenceFromTraceStore, normalizeVoiceProofTrendReport, readVoiceProofTrendReportFile, renderVoiceProofTrendRecommendationHTML, renderVoiceProofTrendRecommendationMarkdown, renderVoiceRealCallProfileHistoryHTML, renderVoiceRealCallProfileHistoryMarkdown, runVoiceRealCallProfileRecoveryLoop, resolveVoiceRealCallProfileProviderRoute } from './proofTrends';
34
34
  export { applyVoiceProfileSwitchGuard, buildVoiceProfileSwitchReadinessReport, buildVoiceProfileSwitchLiveDecisionReport, createVoiceProfileSwitchLiveDecisionRoutes, createVoiceProfileSwitchPolicyProofRoutes, createVoiceProfileSwitchReadinessRoutes, recommendVoiceProfileSwitch, renderVoiceProfileSwitchLiveDecisionHTML, renderVoiceProfileSwitchPolicyProofHTML, renderVoiceProfileSwitchReadinessHTML, runVoiceProfileSwitchPolicyProof } from './profileSwitchRecommendation';
35
35
  export type { VoiceProfileSwitchGuardAction, VoiceProfileSwitchGuardDecision, VoiceProfileSwitchGuardMode, VoiceProfileSwitchGuardOptions, VoiceProfileSwitchObservedSignals, VoiceProfileSwitchLiveDecisionEvidence, VoiceProfileSwitchLiveDecisionReport, VoiceProfileSwitchLiveDecisionReportOptions, VoiceProfileSwitchLiveDecisionRoutesOptions, VoiceProfileSwitchLiveDecisionSession, VoiceProfileSwitchPolicyProofCase, VoiceProfileSwitchPolicyProofCaseResult, VoiceProfileSwitchPolicyProofOptions, VoiceProfileSwitchPolicyProofReport, VoiceProfileSwitchPolicyProofRoutesOptions, VoiceProfileSwitchReadinessIssue, VoiceProfileSwitchReadinessOptions, VoiceProfileSwitchReadinessReport, VoiceProfileSwitchReadinessRoutesOptions, VoiceProfileSwitchReadinessStatus, VoiceProfileSwitchRecommendation, VoiceProfileSwitchRecommendationOptions } from './profileSwitchRecommendation';
36
36
  export { buildVoiceProviderDecisionTraceReport, createVoiceProviderDecisionTraceEvent, createVoiceProviderDecisionTraceRoutes, listVoiceProviderDecisionTraces, renderVoiceProviderDecisionTraceHTML, renderVoiceProviderDecisionTraceMarkdown } from './providerDecisionTraces';
37
37
  export type { VoiceProviderDecisionStatus, VoiceProviderDecisionSurfaceReport, VoiceProviderDecisionTrace, VoiceProviderDecisionTraceInput, VoiceProviderDecisionTraceIssue, VoiceProviderDecisionTraceReport, VoiceProviderDecisionTraceReportOptions, VoiceProviderDecisionTraceRoutesOptions } from './providerDecisionTraces';
38
- export type { VoiceProofTrendAssertionInput, VoiceProofTrendAssertionReport, VoiceProofTrendCycle, VoiceProofTrendProfileDefinition, VoiceProofTrendProfileRecommendation, VoiceProofTrendProfileSummaryOptions, VoiceProofTrendProfileSummary, VoiceProofTrendProviderRecommendation, VoiceProofTrendProviderSummary, VoiceProofTrendRecommendation, VoiceProofTrendRecommendationOptions, VoiceProofTrendRecommendationReport, VoiceProofTrendRecommendationRoutesOptions, VoiceProofTrendRecommendationStatus, VoiceProofTrendRecommendationSurface, VoiceProofTrendRealCallProfileEvidence, VoiceProofTrendRealCallProfileReportOptions, VoiceProofTrendReport, VoiceProofTrendReportInput, VoiceProofTrendRoutesOptions, VoiceProofTrendRuntimeChannelSummary, VoiceProofTrendStatus, VoiceProofTrendSummary, VoiceRealCallProfileDefault, VoiceRealCallProfileDefaultsOptions, VoiceRealCallProfileDefaultsReport, VoiceRealCallProfileHistoryOptions, VoiceRealCallProfileHistoryReport, VoiceRealCallProfileHistoryRoutesOptions, VoiceRealCallProfileProviderRouteOptions, VoiceRealCallProfileReadinessCheckOptions, VoiceRealCallProfileRecoveryActionOptions, VoiceRealCallProfileRecoveryAction, VoiceRealCallProfileRecoveryActionHandler, VoiceRealCallProfileRecoveryActionHandlerInput, VoiceRealCallProfileRecoveryActionId, VoiceRealCallProfileRecoveryJobHistoryCheckOptions, VoiceRealCallProfileRecoveryActionResult, VoiceRealCallProfileRecoveryActionRoutesOptions, VoiceRealCallProfileRecoveryJob, VoiceRealCallProfileRecoveryJobCreateInput, VoiceRealCallProfileRecoveryJobListOptions, VoiceRealCallProfileRecoveryJobStatus, VoiceRealCallProfileRecoveryJobStore, VoiceRealCallProfileRecoveryJobUpdate, VoiceSQLiteRealCallProfileRecoveryJobStoreOptions, VoiceRealCallProfileTraceCollector, VoiceRealCallProfileTraceCollectorEvidenceOptions, VoiceRealCallProfileTraceCollectorOptions, VoiceRealCallProfileTraceEvidenceOptions, VoiceRealCallProfileTraceStoreEvidenceOptions } from './proofTrends';
38
+ export type { VoiceProofTrendAssertionInput, VoiceProofTrendAssertionReport, VoiceProofTrendCycle, VoiceProofTrendProfileDefinition, VoiceProofTrendProfileRecommendation, VoiceProofTrendProfileSummaryOptions, VoiceProofTrendProfileSummary, VoiceProofTrendProviderRecommendation, VoiceProofTrendProviderSummary, VoiceProofTrendRecommendation, VoiceProofTrendRecommendationOptions, VoiceProofTrendRecommendationReport, VoiceProofTrendRecommendationRoutesOptions, VoiceProofTrendRecommendationStatus, VoiceProofTrendRecommendationSurface, VoiceProofTrendRealCallProfileEvidence, VoiceProofTrendRealCallProfileReportOptions, VoiceProofTrendReport, VoiceProofTrendReportInput, VoiceProofTrendRoutesOptions, VoiceProofTrendRuntimeChannelSummary, VoiceProofTrendStatus, VoiceProofTrendSummary, VoiceRealCallProfileDefault, VoiceRealCallProfileDefaultsOptions, VoiceRealCallProfileDefaultsReport, VoiceRealCallProfileHistoryOptions, VoiceRealCallProfileHistoryReport, VoiceRealCallProfileHistoryRoutesOptions, VoiceRealCallProfileProviderRouteOptions, VoiceRealCallProfileReadinessCheckOptions, VoiceRealCallProfileRecoveryActionOptions, VoiceRealCallProfileRecoveryAction, VoiceRealCallProfileRecoveryActionHandler, VoiceRealCallProfileRecoveryActionHandlerInput, VoiceRealCallProfileRecoveryActionId, VoiceRealCallProfileRecoveryJobHistoryCheckOptions, VoiceRealCallProfileRecoveryActionResult, VoiceRealCallProfileRecoveryActionRoutesOptions, VoiceRealCallProfileRecoveryJob, VoiceRealCallProfileRecoveryJobCreateInput, VoiceRealCallProfileRecoveryJobListOptions, VoiceRealCallProfileRecoveryJobStatus, VoiceRealCallProfileRecoveryJobStore, VoiceRealCallProfileRecoveryJobUpdate, VoiceRealCallProfileRecoveryLoopAction, VoiceRealCallProfileRecoveryLoopJob, VoiceRealCallProfileRecoveryLoopJobResult, VoiceRealCallProfileRecoveryLoopOptions, VoiceRealCallProfileRecoveryLoopReport, VoiceRealCallProfileRecoveryLoopStartFailure, VoiceSQLiteRealCallProfileRecoveryJobStoreOptions, VoiceRealCallProfileTraceCollector, VoiceRealCallProfileTraceCollectorEvidenceOptions, VoiceRealCallProfileTraceCollectorOptions, VoiceRealCallProfileTraceEvidenceOptions, VoiceRealCallProfileTraceStoreEvidenceOptions } from './proofTrends';
39
39
  export { assertVoiceSloCalibration, buildVoiceSloCalibrationReport, buildVoiceSloReadinessThresholdReport, createVoiceSloReadinessThresholdOptions, createVoiceSloReadinessThresholdRoutes, createVoiceSloThresholdProfile, createVoiceSloCalibrationRoutes, renderVoiceSloCalibrationMarkdown, renderVoiceSloReadinessThresholdHTML, renderVoiceSloReadinessThresholdMarkdown } from './sloCalibration';
40
40
  export type { VoiceSloCalibrationMetricKey, VoiceSloCalibrationOptions, VoiceSloCalibrationReport, VoiceSloCalibrationRoutesOptions, VoiceSloCalibrationSample, VoiceSloCalibrationStatus, VoiceSloCalibrationThreshold, VoiceSloCalibrationThresholds, VoiceSloReadinessThresholdReport, VoiceSloReadinessThresholdReportOptions, VoiceSloReadinessThresholdOptions, VoiceSloReadinessThresholdRoutesOptions, VoiceSloThresholdProfile } from './sloCalibration';
41
41
  export { assertVoiceLiveOpsControlEvidence, assertVoiceLiveOpsEvidence, buildVoiceLiveOpsControlState, createVoiceLiveOpsController, createVoiceLiveOpsRoutes, createVoiceMemoryLiveOpsControlStore, evaluateVoiceLiveOpsControlEvidence, evaluateVoiceLiveOpsEvidence, getVoiceLiveOpsControlStatus, VOICE_LIVE_OPS_ACTIONS } from './liveOps';
package/dist/index.js CHANGED
@@ -16155,6 +16155,136 @@ var appendRealCallRecoveryActionQuery = (href, query) => {
16155
16155
  const search = new URLSearchParams(entries).toString();
16156
16156
  return `${base}${separator}${search}${hash ? `#${hash}` : ""}`;
16157
16157
  };
16158
+ var sleepVoiceRealCallProfileRecoveryLoop = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
16159
+ var describeVoiceRealCallProfileRecoveryLoopAction = (action) => [
16160
+ action.label ?? action.id ?? "recovery action",
16161
+ action.profileId ? `profile=${action.profileId}` : undefined,
16162
+ action.href
16163
+ ].filter(Boolean).join(" ");
16164
+ var defaultVoiceRealCallProfileRecoveryLoopActionFilter = (action, readinessCheckLabel) => action.method?.toUpperCase() === "POST" && action.sourceCheckLabel === readinessCheckLabel && typeof action.href === "string" && action.href.length > 0;
16165
+ var uniqueVoiceRealCallProfileRecoveryLoopActions = (actions) => {
16166
+ const seen = new Set;
16167
+ return actions.filter((action) => {
16168
+ const key = `${action.method?.toUpperCase() ?? "GET"} ${action.href ?? ""}`;
16169
+ if (seen.has(key)) {
16170
+ return false;
16171
+ }
16172
+ seen.add(key);
16173
+ return true;
16174
+ });
16175
+ };
16176
+ var runVoiceRealCallProfileRecoveryLoop = async (options) => {
16177
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
16178
+ const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
16179
+ const jobPollMs = options.jobPollMs ?? 1200;
16180
+ const jobTimeoutMs = options.jobTimeoutMs ?? 600000;
16181
+ const readinessCheckLabel = options.readinessCheckLabel ?? "Real-call profile history";
16182
+ const fetchImpl = options.fetch ?? fetch;
16183
+ const recoveryActionsHref = options.recoveryActionsHref ?? "/api/production-readiness/recovery-actions";
16184
+ const readinessHref = options.readinessHref ?? "/api/production-readiness";
16185
+ const refreshHref = options.refreshHref === undefined ? "/api/voice/real-call-profile-history/refresh" : options.refreshHref;
16186
+ const jobHref = options.jobHref ?? "/api/voice/real-call-profile-history/actions";
16187
+ const toAbsoluteUrl = (href) => new URL(href, baseUrl).toString();
16188
+ const parseJson = async (response) => {
16189
+ const text = await response.text();
16190
+ try {
16191
+ return JSON.parse(text);
16192
+ } catch (error) {
16193
+ throw new Error(`Expected JSON from ${response.url}, got: ${text.slice(0, 300)}`, { cause: error });
16194
+ }
16195
+ };
16196
+ const fetchJson = async (href, init) => {
16197
+ const response = await fetchImpl(toAbsoluteUrl(href), {
16198
+ headers: { accept: "application/json", ...init?.headers },
16199
+ ...init,
16200
+ signal: init?.signal ?? AbortSignal.timeout(requestTimeoutMs)
16201
+ });
16202
+ if (!response.ok) {
16203
+ throw new Error(`${href} returned HTTP ${String(response.status)}.`);
16204
+ }
16205
+ return parseJson(response);
16206
+ };
16207
+ const resolveJobHref = (jobId) => typeof jobHref === "function" ? jobHref(jobId) : `${jobHref.replace(/\/$/, "")}/${jobId}`;
16208
+ const getGate = async (fresh = false) => {
16209
+ const href = fresh ? `${readinessHref}${readinessHref.includes("?") ? "&" : "?"}voiceRecoveryLoopFresh=${String(Date.now())}` : readinessHref;
16210
+ const readiness = await fetchJson(href);
16211
+ return readiness.checks?.find((check) => check.label === readinessCheckLabel) ?? null;
16212
+ };
16213
+ const actionsResponse = await fetchJson(recoveryActionsHref);
16214
+ const actionFilter = options.actionFilter ?? ((action) => defaultVoiceRealCallProfileRecoveryLoopActionFilter(action, readinessCheckLabel));
16215
+ const actions = uniqueVoiceRealCallProfileRecoveryLoopActions((actionsResponse.actions ?? []).filter(actionFilter));
16216
+ if (actions.length === 0) {
16217
+ const realCallProfileGate2 = await getGate();
16218
+ return {
16219
+ actionCount: 0,
16220
+ actions,
16221
+ jobs: [],
16222
+ ok: realCallProfileGate2?.status === "pass",
16223
+ realCallProfileGate: realCallProfileGate2,
16224
+ startFailures: []
16225
+ };
16226
+ }
16227
+ options.logger?.log(`Running ${String(actions.length)} real-call profile recovery action(s) in parallel.`);
16228
+ for (const action of actions) {
16229
+ options.logger?.log(`- ${describeVoiceRealCallProfileRecoveryLoopAction(action)}`);
16230
+ }
16231
+ const starts = await Promise.allSettled(actions.map(async (action) => {
16232
+ if (!action.href) {
16233
+ throw new Error("Recovery action is missing href.");
16234
+ }
16235
+ const body = await fetchJson(action.href, { method: "POST" });
16236
+ return { action, ...body };
16237
+ }));
16238
+ const startedJobs = starts.flatMap((result) => {
16239
+ if (result.status === "rejected") {
16240
+ return [];
16241
+ }
16242
+ return result.value.jobId ? [result.value] : [];
16243
+ });
16244
+ const startFailures = starts.flatMap((result, index) => result.status === "rejected" ? [
16245
+ {
16246
+ action: describeVoiceRealCallProfileRecoveryLoopAction(actions[index] ?? {}),
16247
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason)
16248
+ }
16249
+ ] : []);
16250
+ const pollJob = async (jobId) => {
16251
+ const deadline = Date.now() + jobTimeoutMs;
16252
+ while (Date.now() < deadline) {
16253
+ const body = await fetchJson(resolveJobHref(jobId));
16254
+ const job = body.job;
16255
+ if (!job) {
16256
+ throw new Error(`Recovery job ${jobId} was not found.`);
16257
+ }
16258
+ if (job.status === "pass" || job.status === "fail") {
16259
+ return job;
16260
+ }
16261
+ await sleepVoiceRealCallProfileRecoveryLoop(jobPollMs);
16262
+ }
16263
+ throw new Error(`Timed out waiting ${String(jobTimeoutMs)}ms for recovery job ${jobId}.`);
16264
+ };
16265
+ options.logger?.log(`Polling ${String(startedJobs.length)} recovery job(s) in parallel.`);
16266
+ const jobResults = await Promise.allSettled(startedJobs.map((start) => pollJob(start.jobId)));
16267
+ const jobs = jobResults.map((result, index) => ({
16268
+ action: describeVoiceRealCallProfileRecoveryLoopAction(startedJobs[index]?.action ?? {}),
16269
+ jobId: startedJobs[index]?.jobId,
16270
+ result: result.status === "fulfilled" ? result.value : {
16271
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason),
16272
+ status: "fail"
16273
+ }
16274
+ }));
16275
+ if (refreshHref !== false) {
16276
+ await fetchJson(refreshHref, { method: "POST" });
16277
+ }
16278
+ const realCallProfileGate = await getGate(true);
16279
+ return {
16280
+ actionCount: actions.length,
16281
+ actions,
16282
+ jobs,
16283
+ ok: startFailures.length === 0 && jobs.every((job) => job.result.status === "pass") && realCallProfileGate?.status === "pass",
16284
+ realCallProfileGate,
16285
+ startFailures
16286
+ };
16287
+ };
16158
16288
  var buildVoiceRealCallProfileRecoveryActions = (report, options = {}) => {
16159
16289
  const actions = [
16160
16290
  {
@@ -16490,21 +16620,22 @@ var buildVoiceRealCallProfileHistoryReport = (options = {}) => {
16490
16620
  ];
16491
16621
  const passingHistory = history.filter((report) => report.ok === true);
16492
16622
  const recommendationHistory = passingHistory.length > 0 ? passingHistory : history;
16493
- const profiles = buildVoiceProofTrendProfileSummaries(recommendationHistory, options);
16623
+ const profileHistory = history.length > 0 ? history : recommendationHistory;
16624
+ const profiles = buildVoiceProofTrendProfileSummaries(profileHistory, options);
16494
16625
  const summary = {
16495
- cycles: recommendationHistory.reduce((total, report) => total + (report.summary.cycles ?? report.cycles.length), 0),
16626
+ cycles: profileHistory.reduce((total, report) => total + (report.summary.cycles ?? report.cycles.length), 0),
16496
16627
  failedReports: history.filter((report) => report.ok !== true).length,
16497
- maxLiveP95Ms: maxNumber(recommendationHistory.map(readProofTrendMaxLiveP95)),
16498
- maxProviderP95Ms: maxNumber(recommendationHistory.map(readProofTrendMaxProviderP95)),
16499
- maxTurnP95Ms: maxNumber(recommendationHistory.map(readProofTrendMaxTurnP95)),
16628
+ maxLiveP95Ms: maxNumber(profileHistory.map(readProofTrendMaxLiveP95)),
16629
+ maxProviderP95Ms: maxNumber(profileHistory.map(readProofTrendMaxProviderP95)),
16630
+ maxTurnP95Ms: maxNumber(profileHistory.map(readProofTrendMaxTurnP95)),
16500
16631
  profileCount: profiles.length,
16501
16632
  profiles,
16502
- providers: readProofTrendProviders(recommendationHistory),
16503
- runtimeChannel: aggregateProofTrendRuntimeChannel(recommendationHistory.map(readProofTrendRuntimeChannel).filter((channel) => channel !== undefined))
16633
+ providers: readProofTrendProviders(profileHistory),
16634
+ runtimeChannel: aggregateProofTrendRuntimeChannel(profileHistory.map(readProofTrendRuntimeChannel).filter((channel) => channel !== undefined))
16504
16635
  };
16505
16636
  const trend = buildVoiceProofTrendReport({
16506
16637
  baseUrl: options.baseUrl,
16507
- cycles: flattenProofTrendCycles(recommendationHistory),
16638
+ cycles: flattenProofTrendCycles(profileHistory),
16508
16639
  generatedAt,
16509
16640
  maxAgeMs: options.maxAgeMs,
16510
16641
  now: options.now,
@@ -39018,6 +39149,7 @@ export {
39018
39149
  runVoiceScenarioFixtureEvals,
39019
39150
  runVoiceScenarioEvals,
39020
39151
  runVoiceReconnectContract,
39152
+ runVoiceRealCallProfileRecoveryLoop,
39021
39153
  runVoiceProviderRoutingContract,
39022
39154
  runVoiceProfileSwitchPolicyProof,
39023
39155
  runVoicePhoneAgentProductionSmokeContract,
@@ -1,6 +1,6 @@
1
1
  import { Elysia } from 'elysia';
2
2
  import type { Database } from 'bun:sqlite';
3
- import type { VoiceProductionReadinessAction, VoiceProductionReadinessCheck } from './productionReadiness';
3
+ import type { VoiceProductionReadinessAction, VoiceProductionReadinessCheck, VoiceReadinessRecoveryAction } from './productionReadiness';
4
4
  import type { StoredVoiceTraceEvent, VoiceTraceEventStore } from './trace';
5
5
  export type VoiceProofTrendStatus = 'empty' | 'fail' | 'pass' | 'stale';
6
6
  export type VoiceProofTrendSummary = {
@@ -469,6 +469,46 @@ export type VoiceRealCallProfileRecoveryActionRoutesOptions = VoiceRealCallProfi
469
469
  handlers?: Partial<Record<VoiceRealCallProfileRecoveryActionId, VoiceRealCallProfileRecoveryActionHandler>>;
470
470
  jobStore?: VoiceRealCallProfileRecoveryJobStore;
471
471
  };
472
+ export type VoiceRealCallProfileRecoveryLoopAction = Partial<VoiceReadinessRecoveryAction> & Partial<VoiceRealCallProfileRecoveryAction> & {
473
+ href?: string;
474
+ method?: string;
475
+ };
476
+ export type VoiceRealCallProfileRecoveryLoopJob = Partial<VoiceRealCallProfileRecoveryJob> & {
477
+ error?: string;
478
+ id?: string;
479
+ status?: string;
480
+ };
481
+ export type VoiceRealCallProfileRecoveryLoopJobResult = {
482
+ action: string;
483
+ jobId?: string;
484
+ result: VoiceRealCallProfileRecoveryLoopJob;
485
+ };
486
+ export type VoiceRealCallProfileRecoveryLoopStartFailure = {
487
+ action: string;
488
+ error: string;
489
+ };
490
+ export type VoiceRealCallProfileRecoveryLoopReport = {
491
+ actionCount: number;
492
+ actions: VoiceRealCallProfileRecoveryLoopAction[];
493
+ jobs: VoiceRealCallProfileRecoveryLoopJobResult[];
494
+ ok: boolean;
495
+ realCallProfileGate: VoiceProductionReadinessCheck | null;
496
+ startFailures: VoiceRealCallProfileRecoveryLoopStartFailure[];
497
+ };
498
+ export type VoiceRealCallProfileRecoveryLoopOptions = {
499
+ actionFilter?: (action: VoiceRealCallProfileRecoveryLoopAction) => boolean;
500
+ baseUrl: string;
501
+ fetch?: typeof fetch;
502
+ jobHref?: string | ((jobId: string) => string);
503
+ jobPollMs?: number;
504
+ jobTimeoutMs?: number;
505
+ logger?: Pick<Console, 'log'>;
506
+ readinessCheckLabel?: string;
507
+ readinessHref?: string;
508
+ recoveryActionsHref?: string;
509
+ refreshHref?: false | string;
510
+ requestTimeoutMs?: number;
511
+ };
472
512
  export declare const DEFAULT_VOICE_PROOF_TRENDS_MAX_AGE_MS: number;
473
513
  export declare const DEFAULT_VOICE_PROOF_TREND_PROFILE_DEFINITIONS: ({
474
514
  description: string;
@@ -503,6 +543,7 @@ export declare const createVoiceRealCallProfileTraceCollector: <TEvent extends S
503
543
  export declare const buildVoiceProofTrendProfileSummaries: (input: VoiceProofTrendReport | readonly VoiceProofTrendReport[], options?: VoiceProofTrendProfileSummaryOptions) => VoiceProofTrendProfileSummary[];
504
544
  export declare const buildVoiceProofTrendReportFromRealCallProfiles: (options: VoiceProofTrendRealCallProfileReportOptions) => VoiceProofTrendReport;
505
545
  export declare const buildVoiceRealCallProfileDefaults: (input: VoiceRealCallProfileHistoryReport | VoiceProofTrendReport, options?: VoiceRealCallProfileDefaultsOptions) => VoiceRealCallProfileDefaultsReport;
546
+ export declare const runVoiceRealCallProfileRecoveryLoop: (options: VoiceRealCallProfileRecoveryLoopOptions) => Promise<VoiceRealCallProfileRecoveryLoopReport>;
506
547
  export declare const buildVoiceRealCallProfileRecoveryActions: (report: VoiceRealCallProfileHistoryReport, options?: VoiceRealCallProfileRecoveryActionOptions) => VoiceRealCallProfileRecoveryAction[];
507
548
  export declare const createVoiceInMemoryRealCallProfileRecoveryJobStore: (options?: {
508
549
  idPrefix?: string;
@@ -2273,6 +2273,136 @@ var appendRealCallRecoveryActionQuery = (href, query) => {
2273
2273
  const search = new URLSearchParams(entries).toString();
2274
2274
  return `${base}${separator}${search}${hash ? `#${hash}` : ""}`;
2275
2275
  };
2276
+ var sleepVoiceRealCallProfileRecoveryLoop = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2277
+ var describeVoiceRealCallProfileRecoveryLoopAction = (action) => [
2278
+ action.label ?? action.id ?? "recovery action",
2279
+ action.profileId ? `profile=${action.profileId}` : undefined,
2280
+ action.href
2281
+ ].filter(Boolean).join(" ");
2282
+ var defaultVoiceRealCallProfileRecoveryLoopActionFilter = (action, readinessCheckLabel) => action.method?.toUpperCase() === "POST" && action.sourceCheckLabel === readinessCheckLabel && typeof action.href === "string" && action.href.length > 0;
2283
+ var uniqueVoiceRealCallProfileRecoveryLoopActions = (actions) => {
2284
+ const seen = new Set;
2285
+ return actions.filter((action) => {
2286
+ const key = `${action.method?.toUpperCase() ?? "GET"} ${action.href ?? ""}`;
2287
+ if (seen.has(key)) {
2288
+ return false;
2289
+ }
2290
+ seen.add(key);
2291
+ return true;
2292
+ });
2293
+ };
2294
+ var runVoiceRealCallProfileRecoveryLoop = async (options) => {
2295
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
2296
+ const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
2297
+ const jobPollMs = options.jobPollMs ?? 1200;
2298
+ const jobTimeoutMs = options.jobTimeoutMs ?? 600000;
2299
+ const readinessCheckLabel = options.readinessCheckLabel ?? "Real-call profile history";
2300
+ const fetchImpl = options.fetch ?? fetch;
2301
+ const recoveryActionsHref = options.recoveryActionsHref ?? "/api/production-readiness/recovery-actions";
2302
+ const readinessHref = options.readinessHref ?? "/api/production-readiness";
2303
+ const refreshHref = options.refreshHref === undefined ? "/api/voice/real-call-profile-history/refresh" : options.refreshHref;
2304
+ const jobHref = options.jobHref ?? "/api/voice/real-call-profile-history/actions";
2305
+ const toAbsoluteUrl = (href) => new URL(href, baseUrl).toString();
2306
+ const parseJson = async (response) => {
2307
+ const text = await response.text();
2308
+ try {
2309
+ return JSON.parse(text);
2310
+ } catch (error) {
2311
+ throw new Error(`Expected JSON from ${response.url}, got: ${text.slice(0, 300)}`, { cause: error });
2312
+ }
2313
+ };
2314
+ const fetchJson = async (href, init) => {
2315
+ const response = await fetchImpl(toAbsoluteUrl(href), {
2316
+ headers: { accept: "application/json", ...init?.headers },
2317
+ ...init,
2318
+ signal: init?.signal ?? AbortSignal.timeout(requestTimeoutMs)
2319
+ });
2320
+ if (!response.ok) {
2321
+ throw new Error(`${href} returned HTTP ${String(response.status)}.`);
2322
+ }
2323
+ return parseJson(response);
2324
+ };
2325
+ const resolveJobHref = (jobId) => typeof jobHref === "function" ? jobHref(jobId) : `${jobHref.replace(/\/$/, "")}/${jobId}`;
2326
+ const getGate = async (fresh = false) => {
2327
+ const href = fresh ? `${readinessHref}${readinessHref.includes("?") ? "&" : "?"}voiceRecoveryLoopFresh=${String(Date.now())}` : readinessHref;
2328
+ const readiness = await fetchJson(href);
2329
+ return readiness.checks?.find((check) => check.label === readinessCheckLabel) ?? null;
2330
+ };
2331
+ const actionsResponse = await fetchJson(recoveryActionsHref);
2332
+ const actionFilter = options.actionFilter ?? ((action) => defaultVoiceRealCallProfileRecoveryLoopActionFilter(action, readinessCheckLabel));
2333
+ const actions = uniqueVoiceRealCallProfileRecoveryLoopActions((actionsResponse.actions ?? []).filter(actionFilter));
2334
+ if (actions.length === 0) {
2335
+ const realCallProfileGate2 = await getGate();
2336
+ return {
2337
+ actionCount: 0,
2338
+ actions,
2339
+ jobs: [],
2340
+ ok: realCallProfileGate2?.status === "pass",
2341
+ realCallProfileGate: realCallProfileGate2,
2342
+ startFailures: []
2343
+ };
2344
+ }
2345
+ options.logger?.log(`Running ${String(actions.length)} real-call profile recovery action(s) in parallel.`);
2346
+ for (const action of actions) {
2347
+ options.logger?.log(`- ${describeVoiceRealCallProfileRecoveryLoopAction(action)}`);
2348
+ }
2349
+ const starts = await Promise.allSettled(actions.map(async (action) => {
2350
+ if (!action.href) {
2351
+ throw new Error("Recovery action is missing href.");
2352
+ }
2353
+ const body = await fetchJson(action.href, { method: "POST" });
2354
+ return { action, ...body };
2355
+ }));
2356
+ const startedJobs = starts.flatMap((result) => {
2357
+ if (result.status === "rejected") {
2358
+ return [];
2359
+ }
2360
+ return result.value.jobId ? [result.value] : [];
2361
+ });
2362
+ const startFailures = starts.flatMap((result, index) => result.status === "rejected" ? [
2363
+ {
2364
+ action: describeVoiceRealCallProfileRecoveryLoopAction(actions[index] ?? {}),
2365
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason)
2366
+ }
2367
+ ] : []);
2368
+ const pollJob = async (jobId) => {
2369
+ const deadline = Date.now() + jobTimeoutMs;
2370
+ while (Date.now() < deadline) {
2371
+ const body = await fetchJson(resolveJobHref(jobId));
2372
+ const job = body.job;
2373
+ if (!job) {
2374
+ throw new Error(`Recovery job ${jobId} was not found.`);
2375
+ }
2376
+ if (job.status === "pass" || job.status === "fail") {
2377
+ return job;
2378
+ }
2379
+ await sleepVoiceRealCallProfileRecoveryLoop(jobPollMs);
2380
+ }
2381
+ throw new Error(`Timed out waiting ${String(jobTimeoutMs)}ms for recovery job ${jobId}.`);
2382
+ };
2383
+ options.logger?.log(`Polling ${String(startedJobs.length)} recovery job(s) in parallel.`);
2384
+ const jobResults = await Promise.allSettled(startedJobs.map((start) => pollJob(start.jobId)));
2385
+ const jobs = jobResults.map((result, index) => ({
2386
+ action: describeVoiceRealCallProfileRecoveryLoopAction(startedJobs[index]?.action ?? {}),
2387
+ jobId: startedJobs[index]?.jobId,
2388
+ result: result.status === "fulfilled" ? result.value : {
2389
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason),
2390
+ status: "fail"
2391
+ }
2392
+ }));
2393
+ if (refreshHref !== false) {
2394
+ await fetchJson(refreshHref, { method: "POST" });
2395
+ }
2396
+ const realCallProfileGate = await getGate(true);
2397
+ return {
2398
+ actionCount: actions.length,
2399
+ actions,
2400
+ jobs,
2401
+ ok: startFailures.length === 0 && jobs.every((job) => job.result.status === "pass") && realCallProfileGate?.status === "pass",
2402
+ realCallProfileGate,
2403
+ startFailures
2404
+ };
2405
+ };
2276
2406
  var buildVoiceRealCallProfileRecoveryActions = (report, options = {}) => {
2277
2407
  const actions = [
2278
2408
  {
@@ -2608,21 +2738,22 @@ var buildVoiceRealCallProfileHistoryReport = (options = {}) => {
2608
2738
  ];
2609
2739
  const passingHistory = history.filter((report) => report.ok === true);
2610
2740
  const recommendationHistory = passingHistory.length > 0 ? passingHistory : history;
2611
- const profiles = buildVoiceProofTrendProfileSummaries(recommendationHistory, options);
2741
+ const profileHistory = history.length > 0 ? history : recommendationHistory;
2742
+ const profiles = buildVoiceProofTrendProfileSummaries(profileHistory, options);
2612
2743
  const summary = {
2613
- cycles: recommendationHistory.reduce((total, report) => total + (report.summary.cycles ?? report.cycles.length), 0),
2744
+ cycles: profileHistory.reduce((total, report) => total + (report.summary.cycles ?? report.cycles.length), 0),
2614
2745
  failedReports: history.filter((report) => report.ok !== true).length,
2615
- maxLiveP95Ms: maxNumber(recommendationHistory.map(readProofTrendMaxLiveP95)),
2616
- maxProviderP95Ms: maxNumber(recommendationHistory.map(readProofTrendMaxProviderP95)),
2617
- maxTurnP95Ms: maxNumber(recommendationHistory.map(readProofTrendMaxTurnP95)),
2746
+ maxLiveP95Ms: maxNumber(profileHistory.map(readProofTrendMaxLiveP95)),
2747
+ maxProviderP95Ms: maxNumber(profileHistory.map(readProofTrendMaxProviderP95)),
2748
+ maxTurnP95Ms: maxNumber(profileHistory.map(readProofTrendMaxTurnP95)),
2618
2749
  profileCount: profiles.length,
2619
2750
  profiles,
2620
- providers: readProofTrendProviders(recommendationHistory),
2621
- runtimeChannel: aggregateProofTrendRuntimeChannel(recommendationHistory.map(readProofTrendRuntimeChannel).filter((channel) => channel !== undefined))
2751
+ providers: readProofTrendProviders(profileHistory),
2752
+ runtimeChannel: aggregateProofTrendRuntimeChannel(profileHistory.map(readProofTrendRuntimeChannel).filter((channel) => channel !== undefined))
2622
2753
  };
2623
2754
  const trend = buildVoiceProofTrendReport({
2624
2755
  baseUrl: options.baseUrl,
2625
- cycles: flattenProofTrendCycles(recommendationHistory),
2756
+ cycles: flattenProofTrendCycles(profileHistory),
2626
2757
  generatedAt,
2627
2758
  maxAgeMs: options.maxAgeMs,
2628
2759
  now: options.now,
package/dist/vue/index.js CHANGED
@@ -2194,6 +2194,136 @@ var appendRealCallRecoveryActionQuery = (href, query) => {
2194
2194
  const search = new URLSearchParams(entries).toString();
2195
2195
  return `${base}${separator}${search}${hash ? `#${hash}` : ""}`;
2196
2196
  };
2197
+ var sleepVoiceRealCallProfileRecoveryLoop = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2198
+ var describeVoiceRealCallProfileRecoveryLoopAction = (action) => [
2199
+ action.label ?? action.id ?? "recovery action",
2200
+ action.profileId ? `profile=${action.profileId}` : undefined,
2201
+ action.href
2202
+ ].filter(Boolean).join(" ");
2203
+ var defaultVoiceRealCallProfileRecoveryLoopActionFilter = (action, readinessCheckLabel) => action.method?.toUpperCase() === "POST" && action.sourceCheckLabel === readinessCheckLabel && typeof action.href === "string" && action.href.length > 0;
2204
+ var uniqueVoiceRealCallProfileRecoveryLoopActions = (actions) => {
2205
+ const seen = new Set;
2206
+ return actions.filter((action) => {
2207
+ const key = `${action.method?.toUpperCase() ?? "GET"} ${action.href ?? ""}`;
2208
+ if (seen.has(key)) {
2209
+ return false;
2210
+ }
2211
+ seen.add(key);
2212
+ return true;
2213
+ });
2214
+ };
2215
+ var runVoiceRealCallProfileRecoveryLoop = async (options) => {
2216
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
2217
+ const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
2218
+ const jobPollMs = options.jobPollMs ?? 1200;
2219
+ const jobTimeoutMs = options.jobTimeoutMs ?? 600000;
2220
+ const readinessCheckLabel = options.readinessCheckLabel ?? "Real-call profile history";
2221
+ const fetchImpl = options.fetch ?? fetch;
2222
+ const recoveryActionsHref = options.recoveryActionsHref ?? "/api/production-readiness/recovery-actions";
2223
+ const readinessHref = options.readinessHref ?? "/api/production-readiness";
2224
+ const refreshHref = options.refreshHref === undefined ? "/api/voice/real-call-profile-history/refresh" : options.refreshHref;
2225
+ const jobHref = options.jobHref ?? "/api/voice/real-call-profile-history/actions";
2226
+ const toAbsoluteUrl = (href) => new URL(href, baseUrl).toString();
2227
+ const parseJson = async (response) => {
2228
+ const text = await response.text();
2229
+ try {
2230
+ return JSON.parse(text);
2231
+ } catch (error) {
2232
+ throw new Error(`Expected JSON from ${response.url}, got: ${text.slice(0, 300)}`, { cause: error });
2233
+ }
2234
+ };
2235
+ const fetchJson = async (href, init) => {
2236
+ const response = await fetchImpl(toAbsoluteUrl(href), {
2237
+ headers: { accept: "application/json", ...init?.headers },
2238
+ ...init,
2239
+ signal: init?.signal ?? AbortSignal.timeout(requestTimeoutMs)
2240
+ });
2241
+ if (!response.ok) {
2242
+ throw new Error(`${href} returned HTTP ${String(response.status)}.`);
2243
+ }
2244
+ return parseJson(response);
2245
+ };
2246
+ const resolveJobHref = (jobId) => typeof jobHref === "function" ? jobHref(jobId) : `${jobHref.replace(/\/$/, "")}/${jobId}`;
2247
+ const getGate = async (fresh = false) => {
2248
+ const href = fresh ? `${readinessHref}${readinessHref.includes("?") ? "&" : "?"}voiceRecoveryLoopFresh=${String(Date.now())}` : readinessHref;
2249
+ const readiness = await fetchJson(href);
2250
+ return readiness.checks?.find((check) => check.label === readinessCheckLabel) ?? null;
2251
+ };
2252
+ const actionsResponse = await fetchJson(recoveryActionsHref);
2253
+ const actionFilter = options.actionFilter ?? ((action) => defaultVoiceRealCallProfileRecoveryLoopActionFilter(action, readinessCheckLabel));
2254
+ const actions = uniqueVoiceRealCallProfileRecoveryLoopActions((actionsResponse.actions ?? []).filter(actionFilter));
2255
+ if (actions.length === 0) {
2256
+ const realCallProfileGate2 = await getGate();
2257
+ return {
2258
+ actionCount: 0,
2259
+ actions,
2260
+ jobs: [],
2261
+ ok: realCallProfileGate2?.status === "pass",
2262
+ realCallProfileGate: realCallProfileGate2,
2263
+ startFailures: []
2264
+ };
2265
+ }
2266
+ options.logger?.log(`Running ${String(actions.length)} real-call profile recovery action(s) in parallel.`);
2267
+ for (const action of actions) {
2268
+ options.logger?.log(`- ${describeVoiceRealCallProfileRecoveryLoopAction(action)}`);
2269
+ }
2270
+ const starts = await Promise.allSettled(actions.map(async (action) => {
2271
+ if (!action.href) {
2272
+ throw new Error("Recovery action is missing href.");
2273
+ }
2274
+ const body = await fetchJson(action.href, { method: "POST" });
2275
+ return { action, ...body };
2276
+ }));
2277
+ const startedJobs = starts.flatMap((result) => {
2278
+ if (result.status === "rejected") {
2279
+ return [];
2280
+ }
2281
+ return result.value.jobId ? [result.value] : [];
2282
+ });
2283
+ const startFailures = starts.flatMap((result, index) => result.status === "rejected" ? [
2284
+ {
2285
+ action: describeVoiceRealCallProfileRecoveryLoopAction(actions[index] ?? {}),
2286
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason)
2287
+ }
2288
+ ] : []);
2289
+ const pollJob = async (jobId) => {
2290
+ const deadline = Date.now() + jobTimeoutMs;
2291
+ while (Date.now() < deadline) {
2292
+ const body = await fetchJson(resolveJobHref(jobId));
2293
+ const job = body.job;
2294
+ if (!job) {
2295
+ throw new Error(`Recovery job ${jobId} was not found.`);
2296
+ }
2297
+ if (job.status === "pass" || job.status === "fail") {
2298
+ return job;
2299
+ }
2300
+ await sleepVoiceRealCallProfileRecoveryLoop(jobPollMs);
2301
+ }
2302
+ throw new Error(`Timed out waiting ${String(jobTimeoutMs)}ms for recovery job ${jobId}.`);
2303
+ };
2304
+ options.logger?.log(`Polling ${String(startedJobs.length)} recovery job(s) in parallel.`);
2305
+ const jobResults = await Promise.allSettled(startedJobs.map((start) => pollJob(start.jobId)));
2306
+ const jobs = jobResults.map((result, index) => ({
2307
+ action: describeVoiceRealCallProfileRecoveryLoopAction(startedJobs[index]?.action ?? {}),
2308
+ jobId: startedJobs[index]?.jobId,
2309
+ result: result.status === "fulfilled" ? result.value : {
2310
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason),
2311
+ status: "fail"
2312
+ }
2313
+ }));
2314
+ if (refreshHref !== false) {
2315
+ await fetchJson(refreshHref, { method: "POST" });
2316
+ }
2317
+ const realCallProfileGate = await getGate(true);
2318
+ return {
2319
+ actionCount: actions.length,
2320
+ actions,
2321
+ jobs,
2322
+ ok: startFailures.length === 0 && jobs.every((job) => job.result.status === "pass") && realCallProfileGate?.status === "pass",
2323
+ realCallProfileGate,
2324
+ startFailures
2325
+ };
2326
+ };
2197
2327
  var buildVoiceRealCallProfileRecoveryActions = (report, options = {}) => {
2198
2328
  const actions = [
2199
2329
  {
@@ -2529,21 +2659,22 @@ var buildVoiceRealCallProfileHistoryReport = (options = {}) => {
2529
2659
  ];
2530
2660
  const passingHistory = history.filter((report) => report.ok === true);
2531
2661
  const recommendationHistory = passingHistory.length > 0 ? passingHistory : history;
2532
- const profiles = buildVoiceProofTrendProfileSummaries(recommendationHistory, options);
2662
+ const profileHistory = history.length > 0 ? history : recommendationHistory;
2663
+ const profiles = buildVoiceProofTrendProfileSummaries(profileHistory, options);
2533
2664
  const summary = {
2534
- cycles: recommendationHistory.reduce((total, report) => total + (report.summary.cycles ?? report.cycles.length), 0),
2665
+ cycles: profileHistory.reduce((total, report) => total + (report.summary.cycles ?? report.cycles.length), 0),
2535
2666
  failedReports: history.filter((report) => report.ok !== true).length,
2536
- maxLiveP95Ms: maxNumber(recommendationHistory.map(readProofTrendMaxLiveP95)),
2537
- maxProviderP95Ms: maxNumber(recommendationHistory.map(readProofTrendMaxProviderP95)),
2538
- maxTurnP95Ms: maxNumber(recommendationHistory.map(readProofTrendMaxTurnP95)),
2667
+ maxLiveP95Ms: maxNumber(profileHistory.map(readProofTrendMaxLiveP95)),
2668
+ maxProviderP95Ms: maxNumber(profileHistory.map(readProofTrendMaxProviderP95)),
2669
+ maxTurnP95Ms: maxNumber(profileHistory.map(readProofTrendMaxTurnP95)),
2539
2670
  profileCount: profiles.length,
2540
2671
  profiles,
2541
- providers: readProofTrendProviders(recommendationHistory),
2542
- runtimeChannel: aggregateProofTrendRuntimeChannel(recommendationHistory.map(readProofTrendRuntimeChannel).filter((channel) => channel !== undefined))
2672
+ providers: readProofTrendProviders(profileHistory),
2673
+ runtimeChannel: aggregateProofTrendRuntimeChannel(profileHistory.map(readProofTrendRuntimeChannel).filter((channel) => channel !== undefined))
2543
2674
  };
2544
2675
  const trend = buildVoiceProofTrendReport({
2545
2676
  baseUrl: options.baseUrl,
2546
- cycles: flattenProofTrendCycles(recommendationHistory),
2677
+ cycles: flattenProofTrendCycles(profileHistory),
2547
2678
  generatedAt,
2548
2679
  maxAgeMs: options.maxAgeMs,
2549
2680
  now: options.now,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.383",
3
+ "version": "0.0.22-beta.385",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",