fix: AI recall-call flow, speech-to-text time parsing, and stale conversation state

- Reset conversation stage to a fresh start on every new voice call
  (recall and reminder alike), and on every hangup/timeout/goodbye,
  so an abandoned or completed call never leaves a patient stuck
  mid-flow for their next call or text.
- Add a staleness check to the SMS flow: after a long idle gap,
  check whether the patient's reply still reads as a continuation
  before resuming the stored stage, falling back to a friendly
  "it's been a while" re-greeting when unsure.
- Fix parseTime() to handle Twilio's speech-to-text output, which
  renders "pm"/"am" with periods and often drops the colon in a
  spoken time (e.g. "100 p.m." -> 1:00 PM) — this was silently
  failing and causing the AI to loop asking for a time it had
  already been given.
- Extend the voice Gather timeout so the AI doesn't hang up on a
  patient who just needs a few seconds to respond.
- Reword the recall-call opening line and rework the post-"yes"
  prompt to ask for day and time together.
- Fix rclone WebDAV server to serve only the backups folder instead
  of the entire backend app directory (was exposing .env and source).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ff
2026-08-06 09:35:48 -04:00
co-authored by Claude Sonnet 5
parent 55ce52f276
commit 0cd095330a
7 changed files with 167 additions and 34 deletions
+20 -3
View File
@@ -392,18 +392,35 @@ async function isSlotAvailable(
// ── Time parsing (legacy) ─────────────────────────────────────────────────────
export async function parseTime(message: string, apiKey: string, provider: AiProvider = "google", model?: string): Promise<string | null> {
const t = message.toLowerCase();
// Twilio speech-to-text usually renders "pm"/"am" with periods ("p.m."),
// and often drops the colon in a spoken time too ("1:00pm" -> "100 pm",
// "12:30pm" -> "1230 pm") — normalize both before matching.
const t = message.toLowerCase()
.replace(/\ba\.m\.?/g, "am")
.replace(/\bp\.m\.?/g, "pm");
if (/\bmorning\b|mañana|manhã|上午|صباح|maten/i.test(t)) return "09:00";
if (/\bafternoon\b|tarde|après-midi|下午|مساء|aprèmidi/i.test(t)) return "13:00";
const ampm = t.match(/\b(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b/);
const ampm = t.match(/\b(\d{1,2}):([0-5]\d)\s*(am|pm)\b/);
const glued = !ampm ? t.match(/\b(\d{1,4})\s*(am|pm)\b/) : null;
const clock = t.match(/\b([01]?\d|2[0-3]):([0-5]\d)\b/);
if (ampm) {
let h = parseInt(ampm[1]!);
const m = ampm[2] ? parseInt(ampm[2]) : 0;
const m = parseInt(ampm[2]!);
if (ampm[3] === "pm" && h < 12) h += 12;
if (ampm[3] === "am" && h === 12) h = 0;
return `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}`;
}
if (glued) {
const digits = glued[1]!;
let h: number, m: number;
if (digits.length <= 2) { h = parseInt(digits); m = 0; }
else { h = parseInt(digits.slice(0, -2)); m = parseInt(digits.slice(-2)); }
if (h >= 1 && h <= 12 && m <= 59) {
if (glued[2] === "pm" && h < 12) h += 12;
if (glued[2] === "am" && h === 12) h = 0;
return `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}`;
}
}
if (clock) return clock[0]!;
try {
const llm = getLlm(provider, apiKey, model);