@absolutejs/voice 0.0.22-beta.622 → 0.0.22-beta.623

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.
@@ -803,6 +803,13 @@ export type VoicePluginConfig<TContext = unknown, TSession extends VoiceSessionR
803
803
  sttRecoveryLine?: string | ((input: {
804
804
  session: TSession;
805
805
  }) => string | Promise<string>);
806
+ stuckCallClose?: {
807
+ afterMs: number;
808
+ line?: string | ((input: {
809
+ session: TSession;
810
+ }) => string | Promise<string>);
811
+ reason?: string;
812
+ };
806
813
  languageStrategy?: VoiceLanguageStrategy;
807
814
  lexicon?: VoiceLexiconEntry[] | VoiceLexiconResolver<TContext>;
808
815
  phraseHints?: VoicePhraseHint[] | VoicePhraseHintResolver<TContext>;
@@ -958,6 +965,20 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
958
965
  sttRecoveryLine?: string | ((input: {
959
966
  session: TSession;
960
967
  }) => string | Promise<string>);
968
+ /** Last-resort GRACEFUL terminal close for a wedged call. If no caller-side
969
+ * progress (committed turn / user partial) lands for `afterMs` on a live call
970
+ * — STT permanently deaf, or the caller left — the assistant speaks `line` and
971
+ * the session COMPLETES (disposition "completed") so onComplete still saves and
972
+ * the call ends with a real goodbye instead of dead air + "abandoned". Reset by
973
+ * real progress (committed turn / user partial / (re)connect), NOT by the
974
+ * assistant's own speech, so STT recovery re-prompts can't defer it forever. */
975
+ stuckCallClose?: {
976
+ afterMs: number;
977
+ line?: string | ((input: {
978
+ session: TSession;
979
+ }) => string | Promise<string>);
980
+ reason?: string;
981
+ };
961
982
  stt?: STTAdapter;
962
983
  realtime?: RealtimeAdapter;
963
984
  realtimeInputFormat?: AudioFormat;
package/dist/index.js CHANGED
@@ -4186,6 +4186,51 @@ var createVoiceSession = (options) => {
4186
4186
  clearCallSilenceWatchdog();
4187
4187
  callSilenceWatchdog = setTimeout(fireCallSilenceTimeout, callSilenceTimeoutMs);
4188
4188
  };
4189
+ const stuckCloseConfig = options.stuckCallClose;
4190
+ const stuckCloseAfterMs = stuckCloseConfig && stuckCloseConfig.afterMs > 0 ? stuckCloseConfig.afterMs : undefined;
4191
+ let stuckCloseWatchdog = null;
4192
+ let stuckCloseFired = false;
4193
+ const clearStuckCloseWatchdog = () => {
4194
+ if (stuckCloseWatchdog) {
4195
+ clearTimeout(stuckCloseWatchdog);
4196
+ stuckCloseWatchdog = null;
4197
+ }
4198
+ };
4199
+ const fireStuckClose = () => {
4200
+ stuckCloseWatchdog = null;
4201
+ if (stuckCloseFired) {
4202
+ return;
4203
+ }
4204
+ stuckCloseFired = true;
4205
+ runSerial("stuck-call-close", async () => {
4206
+ const snapshot = await readSession();
4207
+ if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.call?.endedAt) {
4208
+ return;
4209
+ }
4210
+ await appendTrace({
4211
+ payload: {
4212
+ action: "stuck-call-close",
4213
+ reason: `no caller progress for ${stuckCloseAfterMs}ms`
4214
+ },
4215
+ session: snapshot,
4216
+ type: "session.error"
4217
+ });
4218
+ if (stuckCloseConfig?.line) {
4219
+ await speakResolvedLine(stuckCloseConfig.line, snapshot);
4220
+ }
4221
+ await completeInternal(undefined, {
4222
+ disposition: "completed",
4223
+ reason: stuckCloseConfig?.reason ?? "stuck-call-close"
4224
+ });
4225
+ });
4226
+ };
4227
+ const kickStuckCloseWatchdog = () => {
4228
+ if (stuckCloseAfterMs === undefined || stuckCloseFired) {
4229
+ return;
4230
+ }
4231
+ clearStuckCloseWatchdog();
4232
+ stuckCloseWatchdog = setTimeout(fireStuckClose, stuckCloseAfterMs);
4233
+ };
4189
4234
  const recordingConfig = options.recording;
4190
4235
  const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
4191
4236
  const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
@@ -4682,6 +4727,7 @@ var createVoiceSession = (options) => {
4682
4727
  type: "error"
4683
4728
  });
4684
4729
  clearCallSilenceWatchdog();
4730
+ clearStuckCloseWatchdog();
4685
4731
  clearAmdEvaluationTimer();
4686
4732
  await closeTTSSession("failed");
4687
4733
  await closeAdapter("failed");
@@ -4791,6 +4837,7 @@ var createVoiceSession = (options) => {
4791
4837
  type: "complete"
4792
4838
  });
4793
4839
  clearCallSilenceWatchdog();
4840
+ clearStuckCloseWatchdog();
4794
4841
  clearAmdEvaluationTimer();
4795
4842
  await closeTTSSession("complete");
4796
4843
  await closeAdapter("complete");
@@ -5268,6 +5315,9 @@ var createVoiceSession = (options) => {
5268
5315
  };
5269
5316
  };
5270
5317
  const handlePartial = async (transcript) => {
5318
+ if (transcript.text.trim()) {
5319
+ kickStuckCloseWatchdog();
5320
+ }
5271
5321
  if (activeTTSTurnId !== undefined) {
5272
5322
  const triggeringText = transcript.text.trim();
5273
5323
  if (triggeringText) {
@@ -5742,6 +5792,7 @@ var createVoiceSession = (options) => {
5742
5792
  };
5743
5793
  const completeTurn = async (session, turn) => {
5744
5794
  console.error(`[voice] completeTurn ENTER session=${options.id} turn=${turn.id} textLen=${turn.text?.length ?? 0}`);
5795
+ kickStuckCloseWatchdog();
5745
5796
  const liveOpsControl = await options.liveOps?.getControl(options.id);
5746
5797
  if (liveOpsControl?.assistantPaused || liveOpsControl?.operatorTakeover) {
5747
5798
  await appendTrace({
@@ -6443,6 +6494,7 @@ var createVoiceSession = (options) => {
6443
6494
  await ensureAdapter();
6444
6495
  warmTTSSession();
6445
6496
  kickCallSilenceWatchdog();
6497
+ kickStuckCloseWatchdog();
6446
6498
  startAmdEvaluationTimer();
6447
6499
  if (options.greeting && session.turns.length === 0) {
6448
6500
  await speakResolvedLine(options.greeting, session);
@@ -6588,6 +6640,7 @@ var createVoiceSession = (options) => {
6588
6640
  });
6589
6641
  clearSilenceTimer();
6590
6642
  clearCallSilenceWatchdog();
6643
+ clearStuckCloseWatchdog();
6591
6644
  clearAmdEvaluationTimer();
6592
6645
  if (options.noiseSuppressor?.close) {
6593
6646
  try {
@@ -39705,6 +39758,7 @@ var voice = (config) => {
39705
39758
  greeting: config.greeting,
39706
39759
  resumeGreeting: config.resumeGreeting,
39707
39760
  sttRecoveryLine: config.sttRecoveryLine,
39761
+ stuckCallClose: config.stuckCallClose,
39708
39762
  handoff: config.handoff,
39709
39763
  languageStrategy: config.languageStrategy,
39710
39764
  lexicon,
@@ -6506,6 +6506,51 @@ var createVoiceSession = (options) => {
6506
6506
  clearCallSilenceWatchdog();
6507
6507
  callSilenceWatchdog = setTimeout(fireCallSilenceTimeout, callSilenceTimeoutMs);
6508
6508
  };
6509
+ const stuckCloseConfig = options.stuckCallClose;
6510
+ const stuckCloseAfterMs = stuckCloseConfig && stuckCloseConfig.afterMs > 0 ? stuckCloseConfig.afterMs : undefined;
6511
+ let stuckCloseWatchdog = null;
6512
+ let stuckCloseFired = false;
6513
+ const clearStuckCloseWatchdog = () => {
6514
+ if (stuckCloseWatchdog) {
6515
+ clearTimeout(stuckCloseWatchdog);
6516
+ stuckCloseWatchdog = null;
6517
+ }
6518
+ };
6519
+ const fireStuckClose = () => {
6520
+ stuckCloseWatchdog = null;
6521
+ if (stuckCloseFired) {
6522
+ return;
6523
+ }
6524
+ stuckCloseFired = true;
6525
+ runSerial("stuck-call-close", async () => {
6526
+ const snapshot = await readSession();
6527
+ if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.call?.endedAt) {
6528
+ return;
6529
+ }
6530
+ await appendTrace({
6531
+ payload: {
6532
+ action: "stuck-call-close",
6533
+ reason: `no caller progress for ${stuckCloseAfterMs}ms`
6534
+ },
6535
+ session: snapshot,
6536
+ type: "session.error"
6537
+ });
6538
+ if (stuckCloseConfig?.line) {
6539
+ await speakResolvedLine(stuckCloseConfig.line, snapshot);
6540
+ }
6541
+ await completeInternal(undefined, {
6542
+ disposition: "completed",
6543
+ reason: stuckCloseConfig?.reason ?? "stuck-call-close"
6544
+ });
6545
+ });
6546
+ };
6547
+ const kickStuckCloseWatchdog = () => {
6548
+ if (stuckCloseAfterMs === undefined || stuckCloseFired) {
6549
+ return;
6550
+ }
6551
+ clearStuckCloseWatchdog();
6552
+ stuckCloseWatchdog = setTimeout(fireStuckClose, stuckCloseAfterMs);
6553
+ };
6509
6554
  const recordingConfig = options.recording;
6510
6555
  const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
6511
6556
  const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
@@ -7002,6 +7047,7 @@ var createVoiceSession = (options) => {
7002
7047
  type: "error"
7003
7048
  });
7004
7049
  clearCallSilenceWatchdog();
7050
+ clearStuckCloseWatchdog();
7005
7051
  clearAmdEvaluationTimer();
7006
7052
  await closeTTSSession("failed");
7007
7053
  await closeAdapter("failed");
@@ -7111,6 +7157,7 @@ var createVoiceSession = (options) => {
7111
7157
  type: "complete"
7112
7158
  });
7113
7159
  clearCallSilenceWatchdog();
7160
+ clearStuckCloseWatchdog();
7114
7161
  clearAmdEvaluationTimer();
7115
7162
  await closeTTSSession("complete");
7116
7163
  await closeAdapter("complete");
@@ -7588,6 +7635,9 @@ var createVoiceSession = (options) => {
7588
7635
  };
7589
7636
  };
7590
7637
  const handlePartial = async (transcript) => {
7638
+ if (transcript.text.trim()) {
7639
+ kickStuckCloseWatchdog();
7640
+ }
7591
7641
  if (activeTTSTurnId !== undefined) {
7592
7642
  const triggeringText = transcript.text.trim();
7593
7643
  if (triggeringText) {
@@ -8062,6 +8112,7 @@ var createVoiceSession = (options) => {
8062
8112
  };
8063
8113
  const completeTurn = async (session, turn) => {
8064
8114
  console.error(`[voice] completeTurn ENTER session=${options.id} turn=${turn.id} textLen=${turn.text?.length ?? 0}`);
8115
+ kickStuckCloseWatchdog();
8065
8116
  const liveOpsControl = await options.liveOps?.getControl(options.id);
8066
8117
  if (liveOpsControl?.assistantPaused || liveOpsControl?.operatorTakeover) {
8067
8118
  await appendTrace({
@@ -8763,6 +8814,7 @@ var createVoiceSession = (options) => {
8763
8814
  await ensureAdapter();
8764
8815
  warmTTSSession();
8765
8816
  kickCallSilenceWatchdog();
8817
+ kickStuckCloseWatchdog();
8766
8818
  startAmdEvaluationTimer();
8767
8819
  if (options.greeting && session.turns.length === 0) {
8768
8820
  await speakResolvedLine(options.greeting, session);
@@ -8908,6 +8960,7 @@ var createVoiceSession = (options) => {
8908
8960
  });
8909
8961
  clearSilenceTimer();
8910
8962
  clearCallSilenceWatchdog();
8963
+ clearStuckCloseWatchdog();
8911
8964
  clearAmdEvaluationTimer();
8912
8965
  if (options.noiseSuppressor?.close) {
8913
8966
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.622",
3
+ "version": "0.0.22-beta.623",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",