refactor: standardize developer text and identifiers in English

This commit is contained in:
2026-10-05 10:13:25 -03:00
parent 6b8c27fd76
commit dfc1f663c7
20 changed files with 933 additions and 1142 deletions
+102 -119
View File
@@ -54,16 +54,14 @@ function reportWsDebug(hypothesisId, location, msg, data = {}) {
}
// ---------------------------------------------------------------------------
// 🔎 WS DEBUG TRACER (sempre attivo, 2026-09-24)
// Banner + stile etichette condiviso per tutta la famiglia di log [S2S-WS:*].
// Non è più abilitato solo via localStorage.s2s.debug: gli eventi di vita
// (connect, close, errore) e l'handshake iniziale sono loggati always-on;
// i messaggi bulk (input_audio_buffer.append) vengono solo contati per non
// spammare la Console.
// 🔎 WS DEBUG TRACER (always active, 2026-09-24)
// Shared banner and label styles for all [S2S-WS:*] logs. Lifecycle events
// (connect, close, error) and the initial handshake are always logged.
// Count bulk input_audio_buffer.append messages to avoid console spam.
// ---------------------------------------------------------------------------
(function _wsDebugBanner() {
console.info(
"%c🟡 [S2S-WS] client caricato (ws/s2s-ws-client.js v2026-09-24 23:12) — handshake, stato, messaggi chiave e tool-call loggati; PREFETCH APIPILOT strumenti AD OGNI INIZIO SESSIONE + post-login phone (cache per-utente, cambio phone = refresh=true automatico; invalid. cache globale __s2sInvalidateApipilotCache(); anti-leak token; anti-spam audio).",
"%c🟡 [S2S-WS] client loaded (ws/s2s-ws-client.js v2026-09-24 23:12) — logging handshake, status, key messages and tool calls; PREFETCH APIPILOT tools AT EVERY SESSION START + after phone login (per-user cache, phone changes trigger refresh=true; global invalidation __s2sInvalidateApipilotCache(); token masking; audio log throttling).",
"background:#fef3c7;color:#92400e;font-weight:700;padding:4px 8px;border-radius:4px;"
);
})();
@@ -79,24 +77,22 @@ function _wsLog(status, tag, msg, ...extra) {
const badge = `%c[S2S-WS:${tag}]`;
console.log(`${badge}%c ${msg}`, badgeBg, labelFg, ...extra);
}
/** Contatore per audio chunk bulk, per evitare spam. */
/** Bulk audio chunk counter for throttling logs. */
let _wsMicChunkCount = 0;
let _wsMicChunkLastLogTs = 0;
// ---------------------------------------------------------------------------
// 🔧 APIPILOT PREFETCH AD INIZIO SESSIONE (2026-09-24)
// Ogni volta che s2s-ws-client avvia una connessione Realtime (metodo connect())
// viene scaldata la cache ApiPilot lato server chiamando /api/apipilot/tools.
// Cache TTL lato client 240 s (= TTL server 300 s meno 60 s margine).
// Il prefetch è FIRE-AND-FORGET: non blocca MAI l'handshake WS, non fallisce
// connect() anche se il proxy restituisce 5xx.
// 🔧 APIPILOT PREFETCH AT SESSION START (2026-09-24)
// Each connect() warms the server-side ApiPilot cache via /api/apipilot/tools.
// Client cache TTL is 240 s (server TTL 300 s minus a 60 s margin).
// Prefetch failures, including proxy 5xx responses, do not fail connect().
// ---------------------------------------------------------------------------
/** @type {{at:number, data:any, key:string|null}|null} */
let _apipilotClientCache = null;
const _APIPILOT_CLIENT_TTL_MS = 240_000; // 4 minuti
/** Contatore sessioni totali (per log), incrementato ad ogni connect(). */
const _APIPILOT_CLIENT_TTL_MS = 240_000; // 4 minutes
/** Session counter for logging, incremented by each connect(). */
let _wsSessionCounter = 0;
/** Chiave cache per utente: ultimi 8 cifre phoneNumber o fingerprint token. */
/** Per-user cache key: last 8 phoneNumber digits or a token fingerprint. */
function _apipilotUserKey() {
try {
// @ts-ignore
@@ -108,38 +104,34 @@ function _apipilotUserKey() {
return "anon";
} catch (_) { return "anon"; }
}
/** Invalida la cache ApiPilot lato client. Esporta globalmente per main.js (dopo login phone). */
/** Invalidate the client ApiPilot cache. Exported globally for main.js after phone login. */
function _invalidateApipilotClientCache() {
_apipilotClientCache = null;
console.info(
"%c🔧 [S2S-WS:APIPILOT] Cache client invalidata (nuovo login utente o refresh=true forzato)",
"%c🔧 [S2S-WS:APIPILOT] Client cache invalidated (new user login or forced refresh=true)",
"background:#fde68a;color:#92400e;font-weight:700;padding:1px 6px;border-radius:3px;"
);
}
// Stesso identico marker e regole del lato server Python (server.py#L555-L556).
// Lo ripetiamo identico in JS per garantire:
// (1) stessa classifica dedup
// (2) stesso formato regole marker AI
// (3) dedup by marker stringa <<<APIPILOT_TOOLS_LIST_BEGIN>>> in effectiveReasoningInstructions
// (così non appendiamo due volte se sia server che client provano entrambi).
// Match the Python server's marker and rules for consistent deduplication and
// AI marker syntax. effectiveReasoningInstructions checks this marker so the
// client and server do not append the same tools list twice.
const APIPILOT_PASS1_LIST_MARKER_JS = "<<<APIPILOT_TOOLS_LIST_BEGIN>>>";
/**
* Restituisce il testo del blocco PASSATA-1 da iniettare nel prompt instructions LLM.
* FORMATO IDENTICO al Python _resolve_apipilot_reasoning_context_block(tools=[...]).
* - Inizio con APIPILOT_PASS1_LIST_MARKER_JS
* - Fine con <<<APIPILOT_TOOLS_LIST_END>>>
* - Lingua: SPAGNOLO (stesso di profile inject e istruzioni AI)
* - Cap description a 500 chars
* - Max 100 strumenti
* @param {any[]|null|undefined} tools Lista da cache oppure null (usa cache corrente)
* @returns {string} Blocco di testo PASSATA-1 (stringa vuota se lista vuota / nessuno strumento / nessuna cache)
* Build the PASS-1 block injected into the LLM instructions prompt.
* Match Python _resolve_apipilot_reasoning_context_block(tools=[...]).
* - Start with APIPILOT_PASS1_LIST_MARKER_JS
* - End with <<<APIPILOT_TOOLS_LIST_END>>>
* - Instructions in English; assistant responses remain Spanish
* - Cap descriptions at 500 characters and tools at 100
* @param {any[]|null|undefined} tools Cached list, or null to use the current cache
* @returns {string} PASS-1 text block, empty when no tools or cache are available
*/
function buildApipilotPass1PromptBlock(tools) {
let list = Array.isArray(tools) ? tools : null;
if (!list) {
// Fallback: usa cache client locale corrente (se disponibile)
// Fall back to the current client cache, if available.
const c = _apipilotClientCache;
if (c && c.data && Array.isArray(c.data.tools)) list = c.data.tools;
}
@@ -168,25 +160,24 @@ function buildApipilotPass1PromptBlock(tools) {
console.warn("APIPILOT_PASS1_LIST_MARKER_JS", APIPILOT_PASS1_LIST_MARKER_JS);
const markers = (
`${APIPILOT_PASS1_LIST_MARKER_JS}\n` +
"### Herramientas dinámicas disponibles (ApiPilot AquaPortal)\n" +
"### Available dynamic tools (ApiPilot AquaPortal)\n" +
"\n" +
"Cada herramienta tiene un campo `name` (identificador único, NO lo modifiques, " +
"NO camelCase, NO traducir) y un campo `description` que describe el caso de uso.\n" +
"Each tool has a `name` field (a unique identifier: DO NOT change, camelCase " +
"or translate it) and a `description` field explaining its use case.\n" +
"\n" +
"REGLAS EJECUCIÓN OBLIGATORIAS:\n" +
"1. Si la petición DEL USUARIO (transcript) coincide claramente con la `description` " +
"de EXACTAMENTE una (1) herramienta de la lista inferior → DEBES emitir EXCLUSIVAMENTE " +
"una sola riga come PRIMERA línea de TU RESPUESTA, con questo formato sintattico:\n" +
' <<<APIPILOT_CALL:NAME=nome_herramienta,ARGUMENTS={"parametro1":"valor1","parametro2":"valor2"}>>>\n' +
"2. La parte ARGUMENTS={…} DEBE ser JSON válido (virgolette doppie, virgole, senza " +
"commenti). Includi SOLAMENTE i campi elencati in `required_parameters:` (se ci sono).\n" +
"3. Subito DOPO la riga marker NON aggiungere altro testo (niente spiegazioni, " +
"niente anticipazioni). Il motore eseguirà lo strumento e ti risomministrerà il " +
"risultato in una seconda chiamata.\n" +
"4. Se NESSUNA `description` corrisponde, NON emettere marker e rispondi normalmente.\n" +
"5. Se più di una descrizione corrisponde, scegli la più specifica e usa il marker.\n" +
"MANDATORY EXECUTION RULES:\n" +
"1. If the USER REQUEST (transcript) clearly matches the `description` of " +
"EXACTLY one (1) tool below, output ONLY one line as the FIRST line of YOUR " +
"RESPONSE, using this exact syntax:\n" +
' <<<APIPILOT_CALL:NAME=tool_name,ARGUMENTS={"parameter1":"value1","parameter2":"value2"}>>>\n' +
"2. ARGUMENTS={…} MUST be valid JSON (double quotes, commas, no comments). " +
"Include ONLY fields listed in `required_parameters:`, if any.\n" +
"3. Do NOT add text after the marker line (no explanations or previews). " +
"The engine executes the tool and supplies its result in a second call.\n" +
"4. If NO `description` matches, do NOT emit a marker; respond normally in Spanish.\n" +
"5. If multiple descriptions match, choose the most specific and use the marker.\n" +
"\n" +
"Lista herramientas (name / description):\n" +
"Tools list (name / description):\n" +
`${entries.join("\n")}\n` +
"<<<APIPILOT_TOOLS_LIST_END>>>"
);
@@ -195,7 +186,7 @@ function buildApipilotPass1PromptBlock(tools) {
}
/**
* Controlla se le istruzioni in input contengono già il marker PASS1 (dedup).
* Check whether the input instructions already contain the PASS1 marker.
* @param {string|undefined|null} text
* @returns {boolean}
*/
@@ -204,10 +195,9 @@ function instructionsAlreadyHasApipilotPass1(text) {
}
/**
* Espone globalmente una funzione helper per:
* - main.js: costruire effectiveReasoningInstructions appendendo la lista.
* - altri moduli che volessero leggerla.
* Il check dedup è fatto automaticamente dal chiamante prima di appendere.
* Expose a global helper for main.js to append tools to
* effectiveReasoningInstructions, and for other modules to read the list.
* The caller checks for duplicates before appending.
* @returns {{block: string, count: number, marker: string, alreadyPresent:(t:string)=>boolean}}
*/
function getApipilotPass1ContextInfo() {
@@ -456,12 +446,11 @@ export class S2sWsRealtimeClient extends EventTarget {
*/
async connect() {
if (this._ws) throw new Error("Already connected");
// Contatore sessioni totali: utile nel tracer per correlare gli eventi
// di [S2S-WS:*] a un numero di sessione crescente.
// Correlate [S2S-WS:*] events using an increasing session number.
_wsSessionCounter += 1;
const sessionIdShort = `#${_wsSessionCounter}`;
this._sessionSeq = _wsSessionCounter;
_wsLog(0, "SESSION", `${sessionIdShort} handshake iniziato — status: creating-session`);
_wsLog(0, "SESSION", `${sessionIdShort} handshake started — status: creating-session`);
let connectUrl;
if (this._directUrl) {
@@ -480,7 +469,7 @@ export class S2sWsRealtimeClient extends EventTarget {
// keeps a freed slot from being spent on someone who stepped away, and the
// click is a fresh gesture (re-arms the AudioContext on iOS).
if (waited) {
_wsLog(1, "QUEUE", `${sessionIdShort} tuo turno — attesa click join per confermare slot`);
_wsLog(1, "QUEUE", `${sessionIdShort} your turn — waiting for a join click to confirm the slot`);
await this._awaitJoin(grant);
if (this._closed) throw _codedError("connect aborted", "aborted");
}
@@ -497,38 +486,38 @@ export class S2sWsRealtimeClient extends EventTarget {
}
// Spin up the AudioContext + worklets in parallel with the WS dial,
// E IN PARALLELO scaldiamo ApiPilot (strumenti dinamici AquaPortal).
// Il prefetch è non bloccante: se fallisce, non fallisce connect().
// Also warm ApiPilot (dynamic AquaPortal tools) in parallel.
// A failed prefetch does not fail connect().
const audioReady = this._setupAudio();
const wsReady = this._openWebSocket(connectUrl);
const apipilotReady = this._prefetchApipilotTools(sessionIdShort);
await Promise.all([audioReady, wsReady, apipilotReady.catch(() => void 0)]);
_wsLog(1, "SESSION", `${sessionIdShort} connessione stabilita + audio pipeline + apipilot OK`);
_wsLog(1, "SESSION", `${sessionIdShort} connection established + audio pipeline + apipilot OK`);
}
/**
* 🔧 PRE-FETCH strumenti ApiPilot ad ogni inizio di sessione Realtime.
* - Proxy sul server: GET api/apipilot/tools?include_unauthorized=true
* - Il wrapper FETCH globale traccia come [FETCH:APIPILOT:TOOLS] in Console.
* - Cache TTL locale 240 s per evitare chiamate ridondanti se l'utente
* chiude e riapre sessioni in rapida successione (server TTL=300 s).
* - SE window.__s2sApipilot=false → salta subito.
* - MAI fallisce: sempre catch interno.
* @param {string} sessionIdShort Log tag "sessione #N"
* 🔧 Pre-fetch ApiPilot tools at every Realtime session start.
* - Server proxy: GET api/apipilot/tools?include_unauthorized=true
* - The global FETCH wrapper logs [FETCH:APIPILOT:TOOLS] in the console.
* - Local TTL 240 s avoids redundant calls during rapid session restarts
* (server TTL=300 s).
* - Skip immediately when window.__s2sApipilot=false.
* - Catch failures internally.
* @param {string} sessionIdShort Log tag "session #N"
* @returns {Promise<void>}
*/
async _prefetchApipilotTools(sessionIdShort) {
try {
// @ts-ignore: __s2sApipilot è settato da main.js dopo fetchConfig()
// @ts-ignore: main.js sets __s2sApipilot after fetchConfig().
const enabled = typeof window !== "undefined" && !!window.__s2sApipilot;
if (!enabled) {
_wsLog(2, "APIPILOT", `${sessionIdShort} feature flag apipilot=false sul server — salto prefetch strumenti`);
_wsLog(2, "APIPILOT", `${sessionIdShort} server feature flag apipilot=false — skipping tools prefetch`);
return;
}
const currentUser = _apipilotUserKey();
const now = Date.now();
const cached = _apipilotClientCache;
// HIT cache solo se: (1) ancora fresca TTL (2) stesso utente key (non cambio phone/token tra sessioni)
// Use the cache only within its TTL and for the same user key.
if (cached && (now - cached.at <= _APIPILOT_CLIENT_TTL_MS) && cached.key === currentUser) {
const ageSec = Math.round((now - cached.at) / 1000);
const n =
@@ -537,10 +526,10 @@ export class S2sWsRealtimeClient extends EventTarget {
: Array.isArray(cached.data && cached.data.tools)
? cached.data.tools.length
: "?";
_wsLog(1, "APIPILOT", `${sessionIdShort} HIT cache client strumenti (user=${currentUser}, age=${ageSec}s, N=${n}, TTL=${Math.round(_APIPILOT_CLIENT_TTL_MS/1000)}s)`);
_wsLog(1, "APIPILOT", `${sessionIdShort} HIT client tools cache (user=${currentUser}, age=${ageSec}s, N=${n}, TTL=${Math.round(_APIPILOT_CLIENT_TTL_MS/1000)}s)`);
return;
}
// Propaga header x-demo-session se disponibile in window (per auth fingerprint lato server S2S).
// Forward x-demo-session when available for the S2S server auth fingerprint.
/** @type {Record<string,string>} */
const extraHeaders = {};
try {
@@ -550,11 +539,11 @@ export class S2sWsRealtimeClient extends EventTarget {
extraHeaders["x-demo-session"] = window.__s2sDemoSessionToken;
}
} catch (_) { /* noop */ }
// Usa refresh=true SE è cambiato l'utente (cache.key diversa o null) oppure la cache è stata invalidata globalmente.
// Refresh when the user key changes or the cache is missing/expired.
const needRefresh = !cached || cached.key !== currentUser || (now - cached.at > _APIPILOT_CLIENT_TTL_MS);
const qsRefresh = needRefresh ? "&refresh=true" : "";
const t0 = performance.now();
_wsLog(0, "APIPILOT", `${sessionIdShort} MISS cache (user=${currentUser}, refresh=${needRefresh ? "Y" : "N"}) → GET api/apipilot/tools (proxy a S2S_APIPILOT_TOOLS_URL server)...`);
_wsLog(0, "APIPILOT", `${sessionIdShort} MISS cache (user=${currentUser}, refresh=${needRefresh ? "Y" : "N"}) → GET api/apipilot/tools (server proxy to S2S_APIPILOT_TOOLS_URL)...`);
const res = await fetch(`api/apipilot/tools?include_unauthorized=true${qsRefresh}`, {
method: "GET",
cache: "no-store",
@@ -567,11 +556,11 @@ export class S2sWsRealtimeClient extends EventTarget {
const j = await res.json().catch(() => ({}));
const ok = j && j.success && j.data;
if (!ok) {
throw new Error(`envelope non corretto: success=${Boolean(j && j.success)}`);
throw new Error(`invalid envelope: success=${Boolean(j && j.success)}`);
}
const tools = Array.isArray(j.data.tools) ? j.data.tools : [];
const count = typeof j.data.count === "number" ? j.data.count : tools.length;
// Aggiorna cache locale (in caso di hit la prossima sessione non chiamerà).
// Update the local cache so a subsequent session can reuse it.
_apipilotClientCache = { at: Date.now(), data: { count, tools: tools.slice(0, 100) }, key: currentUser };
const names = tools
.slice(0, 5)
@@ -580,16 +569,16 @@ export class S2sWsRealtimeClient extends EventTarget {
_wsLog(
1,
"APIPILOT",
`${sessionIdShort} READY strumenti APIPILOT caricati: N=${count}, t=${Math.round(dt)}ms, user=${currentUser} [primi 5: ${names || "(nessuno)"}]`
`${sessionIdShort} READY APIPILOT tools loaded: N=${count}, t=${Math.round(dt)}ms, user=${currentUser} [first 5: ${names || "(none)"}]`
);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
_wsLog(
3,
"APIPILOT",
`${sessionIdShort} prefetch FALLITO: ${msg} — diagnostica lato server log [APIPILOT-DISCOVERY]`
`${sessionIdShort} prefetch FAILED: ${msg} — check server logs [APIPILOT-DISCOVERY]`
);
// NON rilanciare: connect() deve comunque andare a buon fine.
// Do not rethrow: connect() must still succeed.
}
}
@@ -670,21 +659,21 @@ export class S2sWsRealtimeClient extends EventTarget {
const dur = (performance.now() - t0).toFixed(1);
if (response.status === 402) {
const body = await response.json().catch(() => ({}));
_wsLog(3, "SESSION", `/session HTTP 402: limite giornaliero esaurito (tier=${body?.tier ?? "?"}) dur=${dur} ms`);
_wsLog(3, "SESSION", `/session HTTP 402: daily limit exhausted (tier=${body?.tier ?? "?"}) dur=${dur} ms`);
throw _codedError("Daily conversation limit reached", "limit", { tier: body?.tier });
}
if (response.status === 503) {
const body = await response.json().catch(() => ({}));
if (body?.state === "at_capacity") {
_wsLog(2, "QUEUE:FULL", `/session HTTP 503 state=at_capacity (coda piena) dur=${dur} ms`);
_wsLog(2, "QUEUE:FULL", `/session HTTP 503 state=at_capacity (queue full) dur=${dur} ms`);
throw _codedError("The queue is full — try again shortly.", "queue-full");
}
_wsLog(3, "SESSION", `/session HTTP 503 (fallback sconosciuto) dur=${dur} ms`);
_wsLog(3, "SESSION", `/session HTTP 503 (unknown fallback) dur=${dur} ms`);
throw new Error("/session failed (503)");
}
if (!response.ok) {
const text = await response.text().catch(() => "");
_wsLog(3, "SESSION", `/session fallita HTTP ${response.status}: ${text.slice(0, 180)} dur=${dur} ms`);
_wsLog(3, "SESSION", `/session failed HTTP ${response.status}: ${text.slice(0, 180)} dur=${dur} ms`);
throw new Error(`/session failed (${response.status}): ${text}`);
}
const json = await response.json();
@@ -692,7 +681,7 @@ export class S2sWsRealtimeClient extends EventTarget {
_wsLog(
2,
"QUEUE:WAIT",
`/session → stato=queued queueId=${json.queue_id ?? "?"} position=${json.position ?? "?"} pollEvery=${json.poll_interval_s ?? "?"}s dur=${dur} ms`
`/session → state=queued queueId=${json.queue_id ?? "?"} position=${json.position ?? "?"} pollEvery=${json.poll_interval_s ?? "?"}s dur=${dur} ms`
);
return {
state: "queued",
@@ -724,7 +713,7 @@ export class S2sWsRealtimeClient extends EventTarget {
_wsLog(
2,
"QUEUE:POLL",
`Inizio poll coda queueId=${ticket.queueId.slice(0, 10)}… initialPos=${ticket.position} intervalMs=${intervalMs}`
`Start queue polling queueId=${ticket.queueId.slice(0, 10)}… initialPos=${ticket.position} intervalMs=${intervalMs}`
);
let pollNum = 0;
@@ -744,11 +733,11 @@ export class S2sWsRealtimeClient extends EventTarget {
if (response.status === 402) {
const body = await response.json().catch(() => ({}));
_wsLog(3, "QUEUE:LIMIT", `poll #${pollNum} HTTP 402 — limite giornaliero (tier=${body?.tier ?? "?"})`);
_wsLog(3, "QUEUE:LIMIT", `poll #${pollNum} HTTP 402 — daily limit (tier=${body?.tier ?? "?"})`);
throw _codedError("Daily conversation limit reached", "limit", { tier: body?.tier });
}
if (response.status === 404) {
_wsLog(3, "QUEUE:EXPIRED", `poll #${pollNum} HTTP 404 — ticket scaduto queueId=${this._queueId.slice(0, 10)}…`);
_wsLog(3, "QUEUE:EXPIRED", `poll #${pollNum} HTTP 404 — ticket expired queueId=${this._queueId.slice(0, 10)}…`);
throw _codedError("Queue timed out", "queue-expired");
}
if (!response.ok) continue; // 502/503 — transient, retry
@@ -763,7 +752,7 @@ export class S2sWsRealtimeClient extends EventTarget {
continue;
}
// Reached the front and claimed a slot.
_wsLog(1, "QUEUE:FRONT", `poll #${pollNum} — RAGGIUNTA la prima posizione. Assegno slot compute.`);
_wsLog(1, "QUEUE:FRONT", `poll #${pollNum} — REACHED the front. Assigning compute slot.`);
this._queueId = "";
return this._parseGrant(json);
}
@@ -881,9 +870,8 @@ export class S2sWsRealtimeClient extends EventTarget {
/** @param {string} connectUrl */
_openWebSocket(connectUrl) {
return new Promise((resolve, reject) => {
// Non loggiamo l'URL completo (potrebbe contenere session_token in
// chiaro nella querystring). Mostriamo solo host + path + un hash della
// parte segreta per audit senza leak token.
// Mask the full URL, which may contain a plaintext session_token.
// Show host + path and secret fingerprints for auditing without leaks.
let maskedUrl = connectUrl;
try {
const u = new URL(connectUrl, window.location.href);
@@ -893,7 +881,7 @@ export class S2sWsRealtimeClient extends EventTarget {
const isSecret = /token|session|auth|jwt|secret|key/i.test(lower) || String(v).length > 16;
secretPortions.push(`${k}=${isSecret ? `✂️…${String(v).slice(-4)} (${String(v).length} chars)` : v}`);
});
maskedUrl = `${u.protocol}//${u.host}${u.pathname} ? ${secretPortions.join(" & ") || "(nessun query param)"}`;
maskedUrl = `${u.protocol}//${u.host}${u.pathname} ? ${secretPortions.join(" & ") || "(no query parameters)"}`;
} catch {
// ignore
}
@@ -905,13 +893,13 @@ export class S2sWsRealtimeClient extends EventTarget {
const onceOpen = () => {
ws.removeEventListener("open", onceOpen);
ws.removeEventListener("error", onceErr);
_wsLog(1, "CONNECT", `WebSocket APERTO su ${maskedUrl} readyState=${ws.readyState}`);
_wsLog(1, "CONNECT", `WebSocket OPEN at ${maskedUrl} readyState=${ws.readyState}`);
resolve();
};
const onceErr = (e) => {
ws.removeEventListener("open", onceOpen);
ws.removeEventListener("error", onceErr);
_wsLog(3, "CONNECT", `WebSocket IMPOSSIBILE da aprire: event.type=${e?.type ?? "error"} url=${maskedUrl}`);
_wsLog(3, "CONNECT", `WebSocket FAILED to open: event.type=${e?.type ?? "error"} url=${maskedUrl}`);
reject(new Error(`WebSocket failed to open: ${e?.type ?? "error"}`));
};
ws.addEventListener("open", onceOpen);
@@ -1037,15 +1025,11 @@ export class S2sWsRealtimeClient extends EventTarget {
`tools=${toolNames || "none"}`
);
}
// Primo ack del server: se startup mode=TALK vogliamo il comportamento
// IDENTICO a quando arriva la frase di wake word "Hola Vocero".
// Quindi NON mandiamo shaped response.create con istruzioni di greet
// hardcodate, ma invece:
// 1. injectiamo "hola vocero" nella conversazione come se l'utente
// l'avesse detto (sendUserText → conversation.item.create user)
// 2. requestResponse() VUOTO → il modello risponde esattamente come
// nel codice wake-word in main.js.
// Stesso identico risultato, nessuna istruzione custom.
// On the first server acknowledgment, TALK startup should behave
// exactly like hearing the "Hola Vocero" wake word:
// 1. Inject "hola vocero" as a user turn through sendUserText().
// 2. Call requestResponse() without custom greeting instructions,
// matching the wake-word path in main.js.
if (this._startWithResponse && !this._initialResponseSent) {
this._initialResponseSent = true;
const phrase = this._startWithResponseText.trim() || "hola vocero";
@@ -1363,7 +1347,7 @@ export class S2sWsRealtimeClient extends EventTarget {
_wsLog(
(ev.code === 1000 || ev.code === 1001) ? 1 : (ev.code ? 3 : 2),
"CLOSE",
`WebSocket CHIUSO code=${ev.code ?? "?"} reason=${String(ev.reason ?? "(nessuna)").slice(0, 160)} wasClean=${ev.wasClean}`
`WebSocket CLOSED code=${ev.code ?? "?"} reason=${String(ev.reason ?? "(none)").slice(0, 160)} wasClean=${ev.wasClean}`
);
if (this._status === "closed" || this._status === "error") return;
if (ev.code === 1000) {
@@ -1395,13 +1379,12 @@ export class S2sWsRealtimeClient extends EventTarget {
},
};
// ───────────────────────────────────────────────────────────────────────────
// 2026-09-25 INIETTA API-PILOT HERRAMIENTAS DINÁMICAS (PASADA-1) nel session.update.
// 3 strati di assicurazione (in ordine di priorità crescente, dedup by marker):
// (A) main.js effectiveReasoningInstructions() ha già appeso la lista.
// (B) NOSTRO server.py proxy realtime ri-inietta (se routing lo attraversa).
// (C) QUI s2s-ws-client: ultimo punto di catch-all prima dell'invio al
// canale, GARANTISCE che la lista sia SEMPRE presente in istruzioni
// (anche se qualcuno fa updateSession() con text sovrascritto poco dopo).
// 2026-09-25 INJECT API-PILOT DYNAMIC TOOLS (PASS 1) in session.update.
// Three safeguards, deduplicated by marker:
// (A) main.js effectiveReasoningInstructions() appends the list.
// (B) The server.py Realtime proxy injects it when routing passes there.
// (C) This client is the final safeguard before sending instructions,
// including after a live updateSession() replaces the prompt.
{
const currentInstr = (typeof session.instructions === "string") ? session.instructions : "";
if (!instructionsAlreadyHasApipilotPass1(currentInstr)) {
@@ -1453,7 +1436,7 @@ export class S2sWsRealtimeClient extends EventTarget {
const session = { type: "realtime" };
if (patch.instructions) {
let instr = patch.instructions;
// ── 2026-09-25 catch-all apipilot PASS-1 anche per patch live. ──
// ── 2026-09-25 ApiPilot PASS-1 safeguard for live patches too. ──
if (typeof instr === "string" && !instructionsAlreadyHasApipilotPass1(instr)) {
try {
const info = typeof window !== "undefined" && typeof window.__s2sGetApipilotPass1ContextInfo === "function"
@@ -1776,8 +1759,8 @@ export class S2sWsRealtimeClient extends EventTarget {
/** @param {Record<string, unknown>} event */
_send(event) {
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
// Anti-spam bulk-rate limiter per eventi audio (~25 al secondo).
// Loggiamo un resoconto ogni 250 chunk (circa 10s a 25Hz) invece di ogni singolo.
// Throttle bulk audio event logs (~25 events per second).
// Log periodic summaries rather than every chunk.
if (event.type === "input_audio_buffer.append") {
_wsMicChunkCount += 1;
const now = performance.now();
@@ -1807,7 +1790,7 @@ export class S2sWsRealtimeClient extends EventTarget {
this._ws.send(payload || JSON.stringify(event));
return;
}
// Messaggi non bulk: log sempre [S2S-WS:SEND] con anteprima del payload.
// Always log non-bulk messages as [S2S-WS:SEND] with a payload preview.
let payloadLength = 0;
let stringifyMs = 0;
let payload = "";