fix voice debug-log privacy and docs drift

omit tool arguments unless content logging is enabled
correct the remaining current tool-count references
This commit is contained in:
manjunath22466 committed 2026-10-05 05:46:47 +05:30
1 parent a697ceae40
commit ae3612a356
5 files changed
+195 -36

No files matched your search

+3 -2
View File
@@ -71,8 +71,9 @@ OPENAI_REALTIME_CONTEXT_RETENTION=0.5
# counted in the session cost meter and cap; off disables them. # counted in the session cost meter and cap; off disables them.
OPENAI_REALTIME_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe OPENAI_REALTIME_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
# The voice debug log (.gev-logs/realtime-conversations.jsonl, rotated at # The voice debug log (.gev-logs/realtime-conversations.jsonl, rotated at
# 32 MB, never expired) omits transcripts and typed or spoken text by default. # 32 MB, never expired) omits transcripts, typed or spoken text, and tool
# Set to 1 to keep them while debugging; delete the log when you are done. # arguments by default. Set to 1 to keep them while debugging; delete the log
# when you are done.
GEV_VOICE_LOG_CONTENT= GEV_VOICE_LOG_CONTENT=
OPENAI_HUD_SUMMARY_MODEL=gpt-5-nano OPENAI_HUD_SUMMARY_MODEL=gpt-5-nano
# Per-IP rate limit for the OpenAI cost endpoints # Per-IP rate limit for the OpenAI cost endpoints
+2 -2
View File
@@ -245,7 +245,7 @@ Click **GEV MIC**, grant the microphone, and just talk. This is more than a voic
- **🎬 Cinematic framing.** _"Show me the planes overhead"_ pulls the camera back, angles it, and frames the live traffic like a director. - **🎬 Cinematic framing.** _"Show me the planes overhead"_ pulls the camera back, angles it, and frames the live traffic like a director.
- **🔒 Honest and secure.** The agent only confirms actions that succeeded. Your `OPENAI_API_KEY` never touches the browser; the client only gets a short-lived session token. - **🔒 Honest and secure.** The agent only confirms actions that succeeded. Your `OPENAI_API_KEY` never touches the browser; the client only gets a short-lived session token.
Twenty-nine tools, four jobs — the commands below come straight from the product's voice test suite and tool playbook: Thirty tools, four jobs — the commands below come straight from the product's voice test suite and tool playbook:
**🎥 Direct it** — drone-operator camera verbs: **🎥 Direct it** — drone-operator camera verbs:
@@ -384,7 +384,7 @@ src/
├── hud.js # Intelligence HUD + AI scene summary ├── hud.js # Intelligence HUD + AI scene summary
├── keySetup.js # POWER UP panel — in-app provider keys (dev server only) ├── keySetup.js # POWER UP panel — in-app provider keys (dev server only)
├── mapStackController.js # Basemap switching — Google 3D / Esri / OSM / ion stacks ├── mapStackController.js # Basemap switching — Google 3D / Esri / OSM / ion stacks
├── voice/ # OpenAI Realtime session + 29 voice tools ├── voice/ # OpenAI Realtime session + 30 voice tools
├── layers/ # Layer components — weather, wind, cyclones, transit, ALPR, … ├── layers/ # Layer components — weather, wind, cyclones, transit, ALPR, …
├── data/ # One module per layer + orchestration + context store ├── data/ # One module per layer + orchestration + context store
│ ├── iconOrientation.js # Screen-projected headings + horizon cull │ ├── iconOrientation.js # Screen-projected headings + horizon cull
+3 -3
View File
@@ -383,8 +383,8 @@ the speaker (`mayVoiceClaimSpeaker()`). Progress lines stay outside the reply
lifecycle and Radio handoff. `turn.span` debug records carry generation and lifecycle and Radio handoff. `turn.span` debug records carry generation and
playback latency, silence, preamble and word-count figures. Caption playback latency, silence, preamble and word-count figures. Caption
transcription (`OPENAI_REALTIME_TRANSCRIBE_MODEL`, default on) is metered into transcription (`OPENAI_REALTIME_TRANSCRIBE_MODEL`, default on) is metered into
the session cost and cap. The debug log omits transcripts and text unless the session cost and cap. The debug log omits transcripts, text and tool
`GEV_VOICE_LOG_CONTENT=1`. arguments unless `GEV_VOICE_LOG_CONTENT=1`.
Point and ask: `pointerContext.js` reads what the cursor is on. A Space hold Point and ask: `pointerContext.js` reads what the cursor is on. A Space hold
is the pointing gesture: its keydown snapshot (released with the key) is sent is the pointing gesture: its keydown snapshot (released with the key) is sent
@@ -2939,7 +2939,7 @@ test:track` 43 tracking invariants · headless QA harnesses under
> ISS pass prediction, and per-layer data attribution. Gate at close: unit 98/98, build clean, > ISS pass prediction, and per-layer data attribution. Gate at close: unit 98/98, build clean,
> track 19/19, + five QA harnesses (heading 16/16, sprites 9/9, cctv 5/5, failstate 5/5, > track 19/19, + five QA harnesses (heading 16/16, sprites 9/9, cctv 5/5, failstate 5/5,
> attribution 18/18). New modules: `src/data/{motionModel,aircraftMeta,aircraftClass,aircraftIcons,issPass,routePlausible,dataCredits}.js`. > attribution 18/18). New modules: `src/data/{motionModel,aircraftMeta,aircraftClass,aircraftIcons,issPass,routePlausible,dataCredits}.js`.
> The live runtime now declares 29 voice tools; the 17→20 count above is retained only as milestone history. > The live runtime now declares 30 voice tools; the 17→20 count above is retained only as milestone history.
## Canonical Docs Order ## Canonical Docs Order
+20 -9
View File
@@ -25,30 +25,41 @@ const REALTIME_DEBUG_LOG_MAX_PER_MIN = 120;
/** /**
* Fields that carry what people said or heard: transcripts, typed and spoken * Fields that carry what people said or heard: transcripts, typed and spoken
* text, streamed deltas and the span's quoted replies. They are dropped from * text, streamed deltas, structured tool arguments and the span's quoted
* the persisted log unless content logging is explicitly enabled * replies. They are dropped from the persisted log unless content logging is explicitly enabled
* (GEV_VOICE_LOG_CONTENT=1), so timing and tool diagnostics never retain * (GEV_VOICE_LOG_CONTENT=1), so timing and tool diagnostics never retain
* speech by default. * speech by default.
*/ */
const VOICE_CONTENT_FIELDS = new Set([ const VOICE_TEXT_FIELDS = new Set([
'transcript', 'transcript',
'text', 'text',
'delta', 'delta',
'answer_text', 'answer_text',
'preamble_text', 'preamble_text',
]); ]);
const VOICE_CONTENT_MAX_DEPTH = 12;
const VOICE_CONTENT_MAX_DEPTH_MARKER = '[omitted: max depth]';
/** Replace spoken or typed content with its length. */ /** Replace spoken or typed content and structured tool arguments. */
function omitVoiceContent(value, depth = 0) { function omitVoiceContent(value, depth = 0) {
if (depth > 12 || value === null || typeof value !== 'object') return value; if (depth > VOICE_CONTENT_MAX_DEPTH) return VOICE_CONTENT_MAX_DEPTH_MARKER;
if (value === null || typeof value !== 'object') return value;
if (Array.isArray(value)) if (Array.isArray(value))
return value.map((item) => omitVoiceContent(item, depth + 1)); return value.map((item) => omitVoiceContent(item, depth + 1));
const output = {}; const output = {};
for (const [key, item] of Object.entries(value)) { for (const [key, item] of Object.entries(value)) {
output[key] = if (key === 'arguments') {
VOICE_CONTENT_FIELDS.has(key) && typeof item === 'string' output[key] =
? `[omitted ${item.length} chars]` typeof item === 'string'
: omitVoiceContent(item, depth + 1); ? `[omitted ${item.length} chars]`
: '[omitted]';
continue;
}
if (VOICE_TEXT_FIELDS.has(key) && typeof item === 'string') {
output[key] = `[omitted ${item.length} chars]`;
continue;
}
output[key] = omitVoiceContent(item, depth + 1);
} }
return output; return output;
} }
+167 -20
View File
@@ -399,37 +399,184 @@ test('OpenAI routes answer generically when the upstream or the request fails',
assert.equal(token.body.includes('fixture-upstream-secret'), false); assert.equal(token.body.includes('fixture-upstream-secret'), false);
}); });
test('the debug-log sink omits what people said unless content logging is enabled', async (t) => { test('the debug-log sink omits what people said and tool arguments unless content logging is enabled', async (t) => {
const record = { const nestObject = (value, levels) => {
event: 'server.event', let nested = value;
payload: { for (let index = 0; index < levels; index += 1) nested = { nested };
type: 'conversation.item.input_audio_transcription.completed', return nested;
transcript: 'my home address is 12 Elm Street',
usage: { input_tokens: 40 },
nested: [{ type: 'input_text', text: 'call my sister' }],
},
span: { answer_text: 'Flying to Tokyo.', answer_words: 3 },
}; };
const nestArray = (value, levels) => {
let nested = value;
for (let index = 0; index < levels; index += 1) nested = [nested];
return nested;
};
const records = [
{
timestamp: '2026-10-05T00:00:00.000Z',
sessionId: 'gev-session-1',
event: 'tool.call',
status: 'active',
payload: {
name: 'analyst_query',
callId: 'call-1',
arguments: { location: '12 Elm Street', note: 'meet Alice there' },
},
},
{
timestamp: '2026-10-05T00:00:01.000Z',
sessionId: 'gev-session-1',
event: 'server.event',
status: 'active',
payload: {
type: 'response.function_call_arguments.done',
eventId: 'event-1',
responseId: 'response-1',
payload: {
type: 'response.function_call_arguments.done',
event_id: 'event-1',
response_id: 'response-1',
item_id: 'item-1',
output_index: 0,
call_id: 'call-1',
name: 'analyst_query',
arguments: '{"query":"Tokyo Station"}',
},
},
},
{
event: 'server.event',
status: 'active',
payload: {
type: 'response.audio_transcript.done',
transcript: 'my home address is 12 Elm Street',
text: 'call my sister',
delta: 'Flying to Tokyo.',
usage: { input_tokens: 40 },
},
},
{
event: 'voice.cost',
status: 'active',
payload: { delta: 17, total: 41 },
},
{
event: 'tool.call',
status: 'active',
payload: { name: 'adjust_camera_zoom', callId: 'call-2', arguments: 2 },
},
{
event: 'deep.object',
status: 'active',
payload: nestObject(
{
transcript: 'deep-object-transcript-secret',
arguments: { query: 'deep-object-argument-secret' },
},
14,
),
},
{
event: 'deep.array',
status: 'active',
payload: nestArray(
{
transcript: 'deep-array-transcript-secret',
arguments: { query: 'deep-array-argument-secret' },
},
14,
),
},
];
for (const includeContent of [false, true]) { for (const includeContent of [false, true]) {
const sourceRoot = root(t); const sourceRoot = root(t);
const handler = createDebugLogHandler({ sourceRoot, includeContent }); const handler = createDebugLogHandler({ sourceRoot, includeContent });
const response = await request(handler, { for (const record of records) {
method: 'POST', const response = await request(handler, {
body: JSON.stringify(record), method: 'POST',
}); body: JSON.stringify(record),
assert.equal(response.status, 204); });
assert.equal(response.status, 204);
}
const logged = readFileSync( const logged = readFileSync(
path.join(sourceRoot, '.gev-logs/realtime-conversations.jsonl'), path.join(sourceRoot, '.gev-logs/realtime-conversations.jsonl'),
'utf8', 'utf8',
); );
assert.equal(logged.includes('Elm Street'), includeContent); assert.equal(logged.includes('Elm Street'), includeContent);
assert.equal(logged.includes('call my sister'), includeContent); assert.equal(logged.includes('call my sister'), includeContent);
assert.equal(logged.includes('meet Alice there'), includeContent);
assert.equal(logged.includes('Tokyo Station'), includeContent);
assert.equal(logged.includes('Flying to Tokyo'), includeContent); assert.equal(logged.includes('Flying to Tokyo'), includeContent);
const parsed = JSON.parse(logged); assert.equal(
assert.equal(parsed.payload.usage.input_tokens, 40, 'numbers stay'); logged.includes('deep-object-transcript-secret'),
assert.equal(parsed.span.answer_words, 3); includeContent,
if (!includeContent) );
assert.equal(parsed.payload.transcript, '[omitted 32 chars]'); assert.equal(
logged.includes('deep-object-argument-secret'),
includeContent,
);
assert.equal(
logged.includes('deep-array-transcript-secret'),
includeContent,
);
assert.equal(logged.includes('deep-array-argument-secret'), includeContent);
const [
toolCall,
serverEvent,
transcriptEvent,
numericDelta,
numericArguments,
deepObject,
deepArray,
] = logged
.split('\n')
.filter(Boolean)
.map((line) => JSON.parse(line));
assert.equal(toolCall.timestamp, records[0].timestamp);
assert.equal(toolCall.sessionId, 'gev-session-1');
assert.equal(toolCall.event, 'tool.call');
assert.equal(toolCall.status, 'active');
assert.equal(toolCall.payload.name, 'analyst_query');
assert.equal(toolCall.payload.callId, 'call-1');
assert.equal(serverEvent.payload.eventId, 'event-1');
assert.equal(serverEvent.payload.responseId, 'response-1');
assert.equal(serverEvent.payload.payload.event_id, 'event-1');
assert.equal(serverEvent.payload.payload.response_id, 'response-1');
assert.equal(serverEvent.payload.payload.item_id, 'item-1');
assert.equal(serverEvent.payload.payload.output_index, 0);
assert.equal(serverEvent.payload.payload.call_id, 'call-1');
assert.equal(serverEvent.payload.payload.name, 'analyst_query');
assert.equal(transcriptEvent.payload.usage.input_tokens, 40);
assert.equal(numericDelta.payload.delta, 17);
assert.equal(numericDelta.payload.total, 41);
if (!includeContent) {
assert.equal(toolCall.payload.arguments, '[omitted]');
assert.match(
serverEvent.payload.payload.arguments,
/^\[omitted \d+ chars\]$/,
);
assert.equal(transcriptEvent.payload.transcript, '[omitted 32 chars]');
assert.equal(transcriptEvent.payload.text, '[omitted 14 chars]');
assert.equal(transcriptEvent.payload.delta, '[omitted 16 chars]');
assert.equal(numericArguments.payload.arguments, '[omitted]');
assert.equal(
JSON.stringify(deepObject.payload).includes('[omitted: max depth]'),
true,
);
assert.equal(
JSON.stringify(deepArray.payload).includes('[omitted: max depth]'),
true,
);
} else {
assert.deepEqual(
toolCall.payload.arguments,
records[0].payload.arguments,
);
assert.equal(
serverEvent.payload.payload.arguments,
records[1].payload.payload.arguments,
);
assert.equal(numericArguments.payload.arguments, 2);
}
} }
}); });