refactor: standardize developer text and identifiers in English
This commit is contained in:
+102
-119
@@ -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 = "";
|
||||
|
||||
Reference in New Issue
Block a user