@absolutejs/voice 0.0.22-beta.477 → 0.0.22-beta.478

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.
@@ -29,6 +29,7 @@ export type VoiceTransferCallToolDestination = {
29
29
  message?: string;
30
30
  metadata?: Record<string, unknown>;
31
31
  target: string;
32
+ transferMode?: "cold" | "warm";
32
33
  };
33
34
  export type VoiceTransferCallToolArgs = {
34
35
  destinationId: string;
package/dist/index.d.ts CHANGED
@@ -120,7 +120,7 @@ export { buildVoiceTraceDeliveryReport, createVoiceTraceDeliveryHTMLHandler, cre
120
120
  export { createVoiceTraceTimelineRoutes, renderVoiceTraceTimelineHTML, renderVoiceTraceTimelineSessionHTML, summarizeVoiceTraceTimeline, } from "./traceTimeline";
121
121
  export { createVoiceSQLiteAuditEventStore, createVoiceSQLiteAuditSinkDeliveryStore, createVoiceSQLiteCampaignStore, createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTelephonyWebhookIdempotencyStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore, } from "./sqliteStore";
122
122
  export { createVoicePostgresAuditEventStore, createVoicePostgresAuditSinkDeliveryStore, createVoicePostgresCampaignStore, createVoicePostgresExternalObjectMapStore, createVoicePostgresIntegrationEventStore, createVoicePostgresReviewStore, createVoicePostgresRuntimeStorage, createVoicePostgresSessionStore, createVoicePostgresTaskStore, createVoicePostgresTelephonyWebhookIdempotencyStore, createVoicePostgresTraceSinkDeliveryStore, createVoicePostgresTraceEventStore, } from "./postgresStore";
123
- export { createVoiceS3ReviewStore } from "./s3Store";
123
+ export { createVoiceS3RecordingStore, createVoiceS3ReviewStore } from "./s3Store";
124
124
  export { createVoiceMemoryStore } from "./memoryStore";
125
125
  export { createVoiceCRMActivitySink, createVoiceHelpdeskTicketSink, createVoiceIntegrationHTTPSink, createVoiceHubSpotTaskSink, createVoiceHubSpotTaskSyncSinks, createVoiceHubSpotTaskUpdateSink, createVoiceLinearIssueSink, createVoiceLinearIssueSyncSinks, createVoiceLinearIssueUpdateSink, createVoiceZendeskTicketSink, createVoiceZendeskTicketSyncSinks, createVoiceZendeskTicketUpdateSink, deliverVoiceIntegrationEventToSinks, } from "./opsSinks";
126
126
  export { createVoiceOpsWebhookEnvelope, createVoiceOpsWebhookReceiverRoutes, createVoiceOpsWebhookSink, verifyVoiceOpsWebhookSignature, } from "./opsWebhook";
package/dist/index.js CHANGED
@@ -3682,6 +3682,30 @@ var createVoiceSession = (options) => {
3682
3682
  const currentTurnAudio = [];
3683
3683
  let fallbackAttemptsForCurrentTurn = 0;
3684
3684
  let fallbackReplayAudioMsForCurrentTurn = 0;
3685
+ const callSilenceTimeoutMs = options.callSilenceTimeoutMs && options.callSilenceTimeoutMs > 0 ? options.callSilenceTimeoutMs : undefined;
3686
+ let callSilenceWatchdog = null;
3687
+ let callSilenceFired = false;
3688
+ const clearCallSilenceWatchdog = () => {
3689
+ if (callSilenceWatchdog) {
3690
+ clearTimeout(callSilenceWatchdog);
3691
+ callSilenceWatchdog = null;
3692
+ }
3693
+ };
3694
+ const fireCallSilenceTimeout = () => {
3695
+ callSilenceWatchdog = null;
3696
+ if (callSilenceFired) {
3697
+ return;
3698
+ }
3699
+ callSilenceFired = true;
3700
+ api.close("silence-timeout");
3701
+ };
3702
+ const kickCallSilenceWatchdog = () => {
3703
+ if (callSilenceTimeoutMs === undefined || callSilenceFired) {
3704
+ return;
3705
+ }
3706
+ clearCallSilenceWatchdog();
3707
+ callSilenceWatchdog = setTimeout(fireCallSilenceTimeout, callSilenceTimeoutMs);
3708
+ };
3685
3709
  const recordingConfig = options.recording;
3686
3710
  const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
3687
3711
  const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
@@ -3970,6 +3994,7 @@ var createVoiceSession = (options) => {
3970
3994
  const sendAssistantAudio = async (chunk, input) => {
3971
3995
  const normalizedChunk = chunk instanceof Uint8Array ? new Uint8Array(chunk) : chunk instanceof ArrayBuffer ? new Uint8Array(chunk.slice(0)) : new Uint8Array(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
3972
3996
  captureRecordingChunk("assistant", normalizedChunk, input.format);
3997
+ kickCallSilenceWatchdog();
3973
3998
  await send({
3974
3999
  chunkBase64: encodeBase64(normalizedChunk),
3975
4000
  format: input.format,
@@ -4191,19 +4216,21 @@ var createVoiceSession = (options) => {
4191
4216
  });
4192
4217
  };
4193
4218
  const transferInternal = async (input) => {
4219
+ const transferMetadata = input.transferMode === undefined ? input.metadata : { ...input.metadata ?? {}, transferMode: input.transferMode };
4194
4220
  const session = await writeSession((currentSession) => {
4195
4221
  pushCallLifecycleEvent(currentSession, {
4196
- metadata: input.metadata,
4222
+ metadata: transferMetadata,
4197
4223
  reason: input.reason,
4198
4224
  target: input.target,
4199
4225
  type: "transfer"
4200
4226
  });
4201
4227
  });
4202
4228
  await appendTrace({
4203
- metadata: input.metadata,
4229
+ metadata: transferMetadata,
4204
4230
  payload: {
4205
4231
  reason: input.reason,
4206
4232
  target: input.target,
4233
+ transferMode: input.transferMode,
4207
4234
  type: "transfer"
4208
4235
  },
4209
4236
  session,
@@ -5234,6 +5261,7 @@ var createVoiceSession = (options) => {
5234
5261
  resumePendingTurnCommit(session);
5235
5262
  await ensureAdapter();
5236
5263
  warmTTSSession();
5264
+ kickCallSilenceWatchdog();
5237
5265
  };
5238
5266
  const disconnectInternal = async (event) => {
5239
5267
  clearSilenceTimer();
@@ -5287,6 +5315,7 @@ var createVoiceSession = (options) => {
5287
5315
  }
5288
5316
  speechDetected = true;
5289
5317
  clearSilenceTimer();
5318
+ kickCallSilenceWatchdog();
5290
5319
  } else if (speechDetected) {
5291
5320
  const currentSession = await readSession();
5292
5321
  const hasTurnText = Boolean(buildTurnText(currentSession.currentTurn.transcripts, currentSession.currentTurn.partialText, {
@@ -5299,44 +5328,49 @@ var createVoiceSession = (options) => {
5299
5328
  }
5300
5329
  await adapter.send(conditionedAudio);
5301
5330
  };
5331
+ const closeInternal = async (reason, disposition = "closed") => {
5332
+ const session = await writeSession((currentSession) => {
5333
+ if (currentSession.status !== "completed" && currentSession.status !== "failed" && !currentSession.call?.endedAt) {
5334
+ currentSession.lastActivityAt = Date.now();
5335
+ currentSession.status = "completed";
5336
+ pushCallLifecycleEvent(currentSession, {
5337
+ disposition,
5338
+ reason,
5339
+ type: "end"
5340
+ });
5341
+ }
5342
+ });
5343
+ clearSilenceTimer();
5344
+ clearCallSilenceWatchdog();
5345
+ await closeTTSSession(reason);
5346
+ await closeAdapter(reason);
5347
+ await persistRecordings();
5348
+ await Promise.resolve(socket.close(1000, reason));
5349
+ if (session.call?.endedAt && session.call.disposition === disposition) {
5350
+ await appendTrace({
5351
+ payload: {
5352
+ disposition,
5353
+ reason,
5354
+ type: "end"
5355
+ },
5356
+ session,
5357
+ type: "call.lifecycle"
5358
+ });
5359
+ await options.route.onCallEnd?.({
5360
+ api,
5361
+ context: options.context,
5362
+ disposition,
5363
+ reason,
5364
+ session
5365
+ });
5366
+ }
5367
+ };
5302
5368
  const api = {
5303
5369
  id: options.id,
5304
5370
  close: async (reason) => {
5305
5371
  await runSerial("api.close", async () => {
5306
- const session = await writeSession((currentSession) => {
5307
- if (currentSession.status !== "completed" && currentSession.status !== "failed" && !currentSession.call?.endedAt) {
5308
- currentSession.lastActivityAt = Date.now();
5309
- currentSession.status = "completed";
5310
- pushCallLifecycleEvent(currentSession, {
5311
- disposition: "closed",
5312
- reason,
5313
- type: "end"
5314
- });
5315
- }
5316
- });
5317
- clearSilenceTimer();
5318
- await closeTTSSession(reason);
5319
- await closeAdapter(reason);
5320
- await persistRecordings();
5321
- await Promise.resolve(socket.close(1000, reason));
5322
- if (session.call?.endedAt && session.call.disposition === "closed") {
5323
- await appendTrace({
5324
- payload: {
5325
- disposition: "closed",
5326
- reason,
5327
- type: "end"
5328
- },
5329
- session,
5330
- type: "call.lifecycle"
5331
- });
5332
- await options.route.onCallEnd?.({
5333
- api,
5334
- context: options.context,
5335
- disposition: "closed",
5336
- reason,
5337
- session
5338
- });
5339
- }
5372
+ const disposition = reason === "silence-timeout" ? "silence-timeout" : "closed";
5373
+ await closeInternal(reason, disposition);
5340
5374
  });
5341
5375
  },
5342
5376
  commitTurn: async (reason = "manual") => runSerial("api.commitTurn", async () => {
@@ -9082,6 +9116,18 @@ var RECIPE_DEFAULTS = {
9082
9116
  defaultQueue: "transfer-verification",
9083
9117
  description: "Creates transfer verification work for transferred calls and escalation work when the handoff fails.",
9084
9118
  escalationQueue: "transfer-escalations"
9119
+ },
9120
+ "cold-transfer": {
9121
+ completedAction: "Verify the SIP REFER landed and the caller reached the destination without re-introduction.",
9122
+ completedDescription: "The call was cold-transferred (SIP REFER) \u2014 confirm the destination picked up.",
9123
+ completedKind: "transfer-check",
9124
+ completedTitle: "Verify cold transfer",
9125
+ defaultCompletedCreatesTask: false,
9126
+ defaultDueInMs: 5 * 60000,
9127
+ defaultPriority: "normal",
9128
+ defaultQueue: "transfer-verification",
9129
+ description: "Creates verification work for cold-transferred (REFER) calls and escalation work when the handoff fails.",
9130
+ escalationQueue: "transfer-escalations"
9085
9131
  }
9086
9132
  };
9087
9133
  var buildRecipeTask = (input) => {
@@ -9211,7 +9257,7 @@ var resolveVoiceOutcomeRecipe = (name, options = {}) => {
9211
9257
  dueInMs: Math.min(options.dueInMs ?? defaults.defaultDueInMs, 20 * 60000),
9212
9258
  name: `${name}-transfer-check`,
9213
9259
  priority: options.priority ?? defaults.defaultPriority,
9214
- queue: name === "warm-transfer" ? options.queue ?? defaults.defaultQueue : "transfer-verification"
9260
+ queue: name === "warm-transfer" || name === "cold-transfer" ? options.queue ?? defaults.defaultQueue : "transfer-verification"
9215
9261
  },
9216
9262
  voicemail: {
9217
9263
  assignee: options.assignee,
@@ -34999,7 +35045,8 @@ ${destinationDocs}`,
34999
35045
  metadata: destination.metadata,
35000
35046
  reason: args?.reason,
35001
35047
  result,
35002
- target: destination.target
35048
+ target: destination.target,
35049
+ transferMode: destination.transferMode
35003
35050
  });
35004
35051
  return {
35005
35052
  destinationId: destination.id,
@@ -41255,6 +41302,62 @@ var createVoiceS3ReviewStore = (options) => {
41255
41302
  set
41256
41303
  };
41257
41304
  };
41305
+ var normalizeRecordingKeyPrefix = (prefix) => prefix?.trim().replace(/^\/+|\/+$/g, "") ?? "voice/recordings";
41306
+ var recordingWavKey = (prefix, sessionId, channel) => `${prefix}/${encodeURIComponent(sessionId)}_${channel}.wav`;
41307
+ var recordingMetadataKey = (prefix, sessionId, channel) => `${prefix}/${encodeURIComponent(sessionId)}_${channel}.json`;
41308
+ var createVoiceS3RecordingStore = (options) => {
41309
+ const client = options.client ?? new Bun.S3Client(options);
41310
+ const keyPrefix = normalizeRecordingKeyPrefix(options.keyPrefix);
41311
+ const publicUrlBase = options.publicUrlBase?.replace(/\/+$/, "");
41312
+ const getFile = (key) => client.file(key, options);
41313
+ const resolveUrl = (key) => publicUrlBase ? `${publicUrlBase}/${key}` : `s3://${key}`;
41314
+ const put = async (artifact) => {
41315
+ const wavKey = recordingWavKey(keyPrefix, artifact.sessionId, artifact.channel);
41316
+ const metadataKey = recordingMetadataKey(keyPrefix, artifact.sessionId, artifact.channel);
41317
+ const wav = encodePcmAsWav(artifact.audioBytes, artifact.format);
41318
+ await getFile(wavKey).write(wav);
41319
+ const recordingUrl = resolveUrl(wavKey);
41320
+ const metadata = {
41321
+ capturedAt: artifact.capturedAt,
41322
+ channel: artifact.channel,
41323
+ durationMs: artifact.durationMs,
41324
+ format: artifact.format,
41325
+ recordingUrl,
41326
+ sessionId: artifact.sessionId
41327
+ };
41328
+ await getFile(metadataKey).write(JSON.stringify(metadata));
41329
+ return {
41330
+ ...artifact,
41331
+ recordingUrl
41332
+ };
41333
+ };
41334
+ const readMetadata = async (sessionId, channel) => {
41335
+ const metadataKey = recordingMetadataKey(keyPrefix, sessionId, channel);
41336
+ const wavKey = recordingWavKey(keyPrefix, sessionId, channel);
41337
+ const metadataFile = getFile(metadataKey);
41338
+ if (!await metadataFile.exists()) {
41339
+ return;
41340
+ }
41341
+ const meta = JSON.parse(await metadataFile.text());
41342
+ const wavBytes = await getFile(wavKey).bytes();
41343
+ return {
41344
+ audioBytes: wavBytes,
41345
+ capturedAt: meta.capturedAt,
41346
+ channel: meta.channel,
41347
+ durationMs: meta.durationMs,
41348
+ format: meta.format,
41349
+ recordingUrl: meta.recordingUrl,
41350
+ sessionId: meta.sessionId
41351
+ };
41352
+ };
41353
+ const get = (sessionId, channel) => readMetadata(sessionId, channel);
41354
+ const list = async (sessionId) => {
41355
+ const channels = ["assistant", "user"];
41356
+ const records = await Promise.all(channels.map((channel) => readMetadata(sessionId, channel)));
41357
+ return records.filter((record) => record !== undefined);
41358
+ };
41359
+ return { get, list, put };
41360
+ };
41258
41361
  // src/memoryStore.ts
41259
41362
  var createVoiceMemoryStore = () => {
41260
41363
  const sessions = new Map;
@@ -45687,6 +45790,7 @@ export {
45687
45790
  createVoiceSQLiteAuditSinkDeliveryStore,
45688
45791
  createVoiceSQLiteAuditEventStore,
45689
45792
  createVoiceS3ReviewStore,
45793
+ createVoiceS3RecordingStore,
45690
45794
  createVoiceS3DeliverySink,
45691
45795
  createVoiceRoutingDecisionSummary,
45692
45796
  createVoiceReviewSavedEvent,
@@ -1,6 +1,6 @@
1
1
  import type { VoiceOpsTaskPriority } from "./ops";
2
2
  import type { VoiceRuntimeOpsConfig, VoiceSessionRecord } from "./types";
3
- export type VoiceOutcomeRecipeName = "appointment-booking" | "lead-qualification" | "support-triage" | "voicemail-callback" | "warm-transfer";
3
+ export type VoiceOutcomeRecipeName = "appointment-booking" | "cold-transfer" | "lead-qualification" | "support-triage" | "voicemail-callback" | "warm-transfer";
4
4
  export type VoiceOutcomeRecipeOptions = {
5
5
  assignee?: string;
6
6
  completedCreatesTask?: boolean;
package/dist/s3Store.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { S3Client, S3Options } from "bun";
2
2
  import type { StoredVoiceCallReviewArtifact, VoiceCallReviewStore } from "./testing/review";
3
+ import type { VoiceRecordingStore } from "./recordingStore";
3
4
  export type VoiceS3ReviewStoreFile = {
4
5
  delete: () => Promise<void>;
5
6
  exists: () => Promise<boolean>;
@@ -12,3 +13,17 @@ export type VoiceS3ReviewStoreOptions = S3Options & {
12
13
  keyPrefix?: string;
13
14
  };
14
15
  export declare const createVoiceS3ReviewStore: <TArtifact extends StoredVoiceCallReviewArtifact = StoredVoiceCallReviewArtifact>(options: VoiceS3ReviewStoreOptions) => VoiceCallReviewStore<TArtifact>;
16
+ export type VoiceS3RecordingStoreFile = {
17
+ delete: () => Promise<void>;
18
+ exists: () => Promise<boolean>;
19
+ text: () => Promise<string>;
20
+ bytes: () => Promise<Uint8Array>;
21
+ write: (data: string | Uint8Array) => Promise<number>;
22
+ };
23
+ export type VoiceS3RecordingStoreClient = Pick<S3Client, "file" | "list">;
24
+ export type VoiceS3RecordingStoreOptions = S3Options & {
25
+ client?: VoiceS3RecordingStoreClient;
26
+ keyPrefix?: string;
27
+ publicUrlBase?: string;
28
+ };
29
+ export declare const createVoiceS3RecordingStore: (options: VoiceS3RecordingStoreOptions) => VoiceRecordingStore;
@@ -5650,6 +5650,30 @@ var createVoiceSession = (options) => {
5650
5650
  const currentTurnAudio = [];
5651
5651
  let fallbackAttemptsForCurrentTurn = 0;
5652
5652
  let fallbackReplayAudioMsForCurrentTurn = 0;
5653
+ const callSilenceTimeoutMs = options.callSilenceTimeoutMs && options.callSilenceTimeoutMs > 0 ? options.callSilenceTimeoutMs : undefined;
5654
+ let callSilenceWatchdog = null;
5655
+ let callSilenceFired = false;
5656
+ const clearCallSilenceWatchdog = () => {
5657
+ if (callSilenceWatchdog) {
5658
+ clearTimeout(callSilenceWatchdog);
5659
+ callSilenceWatchdog = null;
5660
+ }
5661
+ };
5662
+ const fireCallSilenceTimeout = () => {
5663
+ callSilenceWatchdog = null;
5664
+ if (callSilenceFired) {
5665
+ return;
5666
+ }
5667
+ callSilenceFired = true;
5668
+ api.close("silence-timeout");
5669
+ };
5670
+ const kickCallSilenceWatchdog = () => {
5671
+ if (callSilenceTimeoutMs === undefined || callSilenceFired) {
5672
+ return;
5673
+ }
5674
+ clearCallSilenceWatchdog();
5675
+ callSilenceWatchdog = setTimeout(fireCallSilenceTimeout, callSilenceTimeoutMs);
5676
+ };
5653
5677
  const recordingConfig = options.recording;
5654
5678
  const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
5655
5679
  const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
@@ -5938,6 +5962,7 @@ var createVoiceSession = (options) => {
5938
5962
  const sendAssistantAudio = async (chunk, input) => {
5939
5963
  const normalizedChunk = chunk instanceof Uint8Array ? new Uint8Array(chunk) : chunk instanceof ArrayBuffer ? new Uint8Array(chunk.slice(0)) : new Uint8Array(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
5940
5964
  captureRecordingChunk("assistant", normalizedChunk, input.format);
5965
+ kickCallSilenceWatchdog();
5941
5966
  await send({
5942
5967
  chunkBase64: encodeBase64(normalizedChunk),
5943
5968
  format: input.format,
@@ -6159,19 +6184,21 @@ var createVoiceSession = (options) => {
6159
6184
  });
6160
6185
  };
6161
6186
  const transferInternal = async (input) => {
6187
+ const transferMetadata = input.transferMode === undefined ? input.metadata : { ...input.metadata ?? {}, transferMode: input.transferMode };
6162
6188
  const session = await writeSession((currentSession) => {
6163
6189
  pushCallLifecycleEvent(currentSession, {
6164
- metadata: input.metadata,
6190
+ metadata: transferMetadata,
6165
6191
  reason: input.reason,
6166
6192
  target: input.target,
6167
6193
  type: "transfer"
6168
6194
  });
6169
6195
  });
6170
6196
  await appendTrace({
6171
- metadata: input.metadata,
6197
+ metadata: transferMetadata,
6172
6198
  payload: {
6173
6199
  reason: input.reason,
6174
6200
  target: input.target,
6201
+ transferMode: input.transferMode,
6175
6202
  type: "transfer"
6176
6203
  },
6177
6204
  session,
@@ -7202,6 +7229,7 @@ var createVoiceSession = (options) => {
7202
7229
  resumePendingTurnCommit(session);
7203
7230
  await ensureAdapter();
7204
7231
  warmTTSSession();
7232
+ kickCallSilenceWatchdog();
7205
7233
  };
7206
7234
  const disconnectInternal = async (event) => {
7207
7235
  clearSilenceTimer();
@@ -7255,6 +7283,7 @@ var createVoiceSession = (options) => {
7255
7283
  }
7256
7284
  speechDetected = true;
7257
7285
  clearSilenceTimer();
7286
+ kickCallSilenceWatchdog();
7258
7287
  } else if (speechDetected) {
7259
7288
  const currentSession = await readSession();
7260
7289
  const hasTurnText = Boolean(buildTurnText(currentSession.currentTurn.transcripts, currentSession.currentTurn.partialText, {
@@ -7267,44 +7296,49 @@ var createVoiceSession = (options) => {
7267
7296
  }
7268
7297
  await adapter.send(conditionedAudio);
7269
7298
  };
7299
+ const closeInternal = async (reason, disposition = "closed") => {
7300
+ const session = await writeSession((currentSession) => {
7301
+ if (currentSession.status !== "completed" && currentSession.status !== "failed" && !currentSession.call?.endedAt) {
7302
+ currentSession.lastActivityAt = Date.now();
7303
+ currentSession.status = "completed";
7304
+ pushCallLifecycleEvent(currentSession, {
7305
+ disposition,
7306
+ reason,
7307
+ type: "end"
7308
+ });
7309
+ }
7310
+ });
7311
+ clearSilenceTimer();
7312
+ clearCallSilenceWatchdog();
7313
+ await closeTTSSession(reason);
7314
+ await closeAdapter(reason);
7315
+ await persistRecordings();
7316
+ await Promise.resolve(socket.close(1000, reason));
7317
+ if (session.call?.endedAt && session.call.disposition === disposition) {
7318
+ await appendTrace({
7319
+ payload: {
7320
+ disposition,
7321
+ reason,
7322
+ type: "end"
7323
+ },
7324
+ session,
7325
+ type: "call.lifecycle"
7326
+ });
7327
+ await options.route.onCallEnd?.({
7328
+ api,
7329
+ context: options.context,
7330
+ disposition,
7331
+ reason,
7332
+ session
7333
+ });
7334
+ }
7335
+ };
7270
7336
  const api = {
7271
7337
  id: options.id,
7272
7338
  close: async (reason) => {
7273
7339
  await runSerial("api.close", async () => {
7274
- const session = await writeSession((currentSession) => {
7275
- if (currentSession.status !== "completed" && currentSession.status !== "failed" && !currentSession.call?.endedAt) {
7276
- currentSession.lastActivityAt = Date.now();
7277
- currentSession.status = "completed";
7278
- pushCallLifecycleEvent(currentSession, {
7279
- disposition: "closed",
7280
- reason,
7281
- type: "end"
7282
- });
7283
- }
7284
- });
7285
- clearSilenceTimer();
7286
- await closeTTSSession(reason);
7287
- await closeAdapter(reason);
7288
- await persistRecordings();
7289
- await Promise.resolve(socket.close(1000, reason));
7290
- if (session.call?.endedAt && session.call.disposition === "closed") {
7291
- await appendTrace({
7292
- payload: {
7293
- disposition: "closed",
7294
- reason,
7295
- type: "end"
7296
- },
7297
- session,
7298
- type: "call.lifecycle"
7299
- });
7300
- await options.route.onCallEnd?.({
7301
- api,
7302
- context: options.context,
7303
- disposition: "closed",
7304
- reason,
7305
- session
7306
- });
7307
- }
7340
+ const disposition = reason === "silence-timeout" ? "silence-timeout" : "closed";
7341
+ await closeInternal(reason, disposition);
7308
7342
  });
7309
7343
  },
7310
7344
  commitTurn: async (reason = "manual") => runSerial("api.commitTurn", async () => {
package/dist/types.d.ts CHANGED
@@ -271,7 +271,7 @@ export type VoiceSessionSummary = {
271
271
  status: VoiceSessionStatus;
272
272
  turnCount: number;
273
273
  };
274
- export type VoiceCallDisposition = "completed" | "transferred" | "escalated" | "voicemail" | "no-answer" | "failed" | "closed";
274
+ export type VoiceCallDisposition = "completed" | "transferred" | "escalated" | "voicemail" | "no-answer" | "failed" | "silence-timeout" | "closed";
275
275
  export type VoiceCallLifecycleEvent = {
276
276
  at: number;
277
277
  type: "start" | "end" | "transfer" | "escalation" | "voicemail" | "no-answer";
@@ -455,6 +455,7 @@ export type VoiceSessionHandle<TContext = unknown, TSession extends VoiceSession
455
455
  reason?: string;
456
456
  result?: TResult;
457
457
  target: string;
458
+ transferMode?: "cold" | "warm";
458
459
  }) => Promise<void>;
459
460
  close: (reason?: string) => Promise<void>;
460
461
  snapshot: () => Promise<TSession>;
@@ -467,6 +468,7 @@ export type VoiceRouteResult<TResult = unknown> = {
467
468
  metadata?: Record<string, unknown>;
468
469
  reason?: string;
469
470
  target: string;
471
+ transferMode?: "cold" | "warm";
470
472
  };
471
473
  escalate?: {
472
474
  metadata?: Record<string, unknown>;
@@ -722,6 +724,7 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
722
724
  store: VoiceSessionStore<TSession>;
723
725
  trace?: VoiceTraceEventStore;
724
726
  recording?: VoiceSessionRecordingConfig;
727
+ callSilenceTimeoutMs?: number;
725
728
  reconnect: Required<VoiceReconnectConfig>;
726
729
  phraseHints?: VoicePhraseHint[];
727
730
  sessionMetadata?: Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.477",
3
+ "version": "0.0.22-beta.478",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",