mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
- Voice layer manifest (src/voice/layerManifest.js) generates the layer enums, aliases and analyst field hints; a test fails when a catalog layer has no entry. - analyst_query validates fields, operators, units, values and scopes, counts up to 250,000 loaded records per layer, reports complete/partial/unanswered, tells LAYER_OFF / NOT_READY / FEED_UNAVAILABLE apart from zero, refuses mismatched follow-ups and never lets a superseded query replace follow-up memory. Seven more layers expose analyst records. - Tool results carry a code-built say line and a display card; the voice card in the mic control shows captions, plan steps and the result (src/voice/resultDisplay.js is the one adapter). Slow tools narrate their current step out of band; holding Space while the assistant talks cuts it off. Turn spans go to the debug log; caption transcription is metered; the log omits what was said unless GEV_VOICE_LOG_CONTENT=1. - Point and ask: the Space-down pointer snapshot resolves "this", "that one" and "around here" through 'pointer' targets, and "the second one" through referent:n from the last numbered list. - Prompt states each policy once (instructions + tools 48.5k -> 46.7k chars). - Voice benchmark harness (scripts/qa-voice-bench.mjs, scripts/voice-bench/) shares its phrase table and grading with scripts/qa-voice-routing.mjs.
141 lines
5.1 KiB
JavaScript
141 lines
5.1 KiB
JavaScript
import { defaultSourceRoot } from '../common/source-root.js';
|
|
import path from 'node:path';
|
|
import { readRequestBody } from '../common/request.js';
|
|
import { promises as fsp } from 'node:fs';
|
|
import { makeRateLimiter, clientKey } from '../common/rate-limit.js';
|
|
|
|
/** Cap on one request body. */
|
|
const REALTIME_DEBUG_LOG_MAX_BYTES = 8 * 1024 * 1024;
|
|
|
|
/**
|
|
* Ceiling for the live log before it rotates. One generation is kept
|
|
* (`…jsonl.1`), so the sink is bounded at twice this however long a session
|
|
* runs. REALTIME_DEBUG_LOG_MAX_BYTES caps a single request; it never bounded
|
|
* the file those requests accumulate into.
|
|
*/
|
|
const REALTIME_DEBUG_LOG_MAX_FILE_BYTES = 32 * 1024 * 1024;
|
|
|
|
/**
|
|
* Per-IP write ceiling. Always on, unlike the opt-in limiter the cost-bearing
|
|
* OpenAI routes share: throttling those by default would change what a user
|
|
* spends, while this sink spends disk and event-loop time. A voice session
|
|
* writes far below this, so the cap needs no configuration to stay invisible.
|
|
*/
|
|
const REALTIME_DEBUG_LOG_MAX_PER_MIN = 120;
|
|
|
|
/**
|
|
* 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
|
|
* the persisted log unless content logging is explicitly enabled
|
|
* (GEV_VOICE_LOG_CONTENT=1), so timing and tool diagnostics never retain
|
|
* speech by default.
|
|
*/
|
|
const VOICE_CONTENT_FIELDS = new Set([
|
|
'transcript',
|
|
'text',
|
|
'delta',
|
|
'answer_text',
|
|
'preamble_text',
|
|
]);
|
|
|
|
/** Replace spoken or typed content with its length. */
|
|
function omitVoiceContent(value, depth = 0) {
|
|
if (depth > 12 || value === null || typeof value !== 'object') return value;
|
|
if (Array.isArray(value))
|
|
return value.map((item) => omitVoiceContent(item, depth + 1));
|
|
const output = {};
|
|
for (const [key, item] of Object.entries(value)) {
|
|
output[key] =
|
|
VOICE_CONTENT_FIELDS.has(key) && typeof item === 'string'
|
|
? `[omitted ${item.length} chars]`
|
|
: omitVoiceContent(item, depth + 1);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function createDebugLogHandler({
|
|
sourceRoot = defaultSourceRoot,
|
|
includeContent = process.env.GEV_VOICE_LOG_CONTENT === '1',
|
|
} = {}) {
|
|
const logDir = path.join(sourceRoot, '.gev-logs');
|
|
const logFile = path.join(logDir, 'realtime-conversations.jsonl');
|
|
const allow = makeRateLimiter({
|
|
windowMs: 60_000,
|
|
max: REALTIME_DEBUG_LOG_MAX_PER_MIN,
|
|
globalMax: 400,
|
|
});
|
|
|
|
// Appends are chained rather than fired concurrently because the rotation
|
|
// check is a read-then-write on file size: run in parallel, every caller
|
|
// stats a file still under the ceiling, none of them rotates, and the log
|
|
// grows past it. The records themselves are O_APPEND and never interleave.
|
|
let queue = Promise.resolve();
|
|
|
|
function append(line) {
|
|
const done = queue.then(async () => {
|
|
await fsp.mkdir(logDir, { recursive: true });
|
|
const size = await fsp.stat(logFile).then(
|
|
(stat) => stat.size,
|
|
() => 0,
|
|
);
|
|
if (size + Buffer.byteLength(line) > REALTIME_DEBUG_LOG_MAX_FILE_BYTES) {
|
|
// Node does not replace an existing destination on Windows. Remove the
|
|
// retained generation explicitly so every rotation behaves the same on
|
|
// supported platforms rather than failing after the first 64 MB.
|
|
await fsp.rm(`${logFile}.1`, { force: true });
|
|
await fsp.rename(logFile, `${logFile}.1`);
|
|
}
|
|
await fsp.appendFile(logFile, line);
|
|
});
|
|
// The caller still sees its own rejection, but one failed append must not
|
|
// poison the chain for every request queued behind it.
|
|
queue = done.catch(() => {});
|
|
return done;
|
|
}
|
|
|
|
return async (req, res) => {
|
|
if (req.method !== 'POST') {
|
|
res.statusCode = 405;
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
return;
|
|
}
|
|
|
|
if (!allow(clientKey(req))) {
|
|
res.writeHead(429, {
|
|
'Content-Type': 'application/json',
|
|
'Retry-After': '60',
|
|
});
|
|
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const body = await readRequestBody(req, REALTIME_DEBUG_LOG_MAX_BYTES);
|
|
const parsed = JSON.parse(body || '{}');
|
|
const record = includeContent ? parsed : omitVoiceContent(parsed);
|
|
await append(
|
|
`${JSON.stringify({
|
|
loggedAt: new Date().toISOString(),
|
|
...record,
|
|
})}\n`,
|
|
);
|
|
res.statusCode = 204;
|
|
res.end();
|
|
} catch (error) {
|
|
// A malformed record or an oversized body is the caller's fault; a failed
|
|
// write is ours, and only the second is worth a log line. Neither answer
|
|
// carries the error text, which for an fs failure is an errno and an
|
|
// absolute path.
|
|
const callerFault =
|
|
error instanceof SyntaxError || error?.code === 'BODY_TOO_LARGE';
|
|
if (!callerFault) console.warn('[realtime-debug-log] append failed');
|
|
res.statusCode = callerFault ? 400 : 500;
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ error: 'Failed to write Realtime debug log' }));
|
|
}
|
|
};
|
|
}
|
|
|
|
export { createDebugLogHandler, omitVoiceContent };
|