mirror of
https://github.com/Heretek-AI/GDevelop-Unlocked.git
synced 2026-11-07 10:55:58 -10:00
- IPC debug logger bridge: - Add IPC bridge from launcher/preload-unlock.cjs to launcher/heretek-hook.cjs via 'heretek:debug-log' - Support customSource in launcher/debug-logger.cjs to record renderer events (source: 'renderer') directly to JSONL disk logs - Prevent losing renderer errors, warnings, and turn events previously buffered only in-memory - AI Agent request & telemetry capture: - Instrument sendChatCompletion in packages/ai-studio/AI/CustomAIClient.js to emit ai-request-sent, ai-response-received, and ai-request-error - Record endpointUrl, model, prompt messages, tools, durationMs, and provider token usage - Instrument LoopGuard.js with [TOOL_CALL] and [TOOL_RESULT] carrying callId, arguments, repeat counts, and error counts - Add safeFormatMessagesForDebug helper for bounded, safe message serialization - Tool audit & diagnostics: - Add standalone parser scripts/audit_agent_log.py and comprehensive report AUDIT_AGENT_LOG_REPORT.md auditing 67 agent interaction rows - Verification & tests: - Update packages/ai-studio/MANIFEST.json sha256 checksums and byte counts - Add unit tests in tests/launcher/debug-logger.test.cjs covering IPC event forwarding and disk serialization - Full test suite passes: 237/237 passing (+2 new tests)
143 lines
4.0 KiB
JavaScript
143 lines
4.0 KiB
JavaScript
// Client debug logging (T02): sync JSONL file appender for the launcher.
|
|
//
|
|
// Activated only by `--heretek-debug-log <path>` or the
|
|
// `HERETEK_DEBUG_LOG` env var; without either, `resolveDebugLogPath`
|
|
// returns null and nothing is created. Every record is one JSON object
|
|
// per line: {ts, iso, source, level, event, data}.
|
|
//
|
|
// Zero npm dependencies (node builtins only — pinned by
|
|
// tests/launcher/debug-logger.test.cjs). Writes are synchronous
|
|
// `fs.appendFileSync` so a crash never loses the line that explains it.
|
|
// An invalid path disables the logger instead of throwing: every method
|
|
// becomes a no-op returning null and the cause is kept on `.error`.
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
|
|
const DEBUG_LOG_ARG = '--heretek-debug-log';
|
|
const DEBUG_LOG_ENV = 'HERETEK_DEBUG_LOG';
|
|
|
|
const LEVELS = ['debug', 'info', 'warn', 'error'];
|
|
|
|
/**
|
|
* Where to log, or null when debug logging is off. The argv flag wins;
|
|
* a flag with no following path is treated as inactive (never a crash).
|
|
*/
|
|
function resolveDebugLogPath(opts) {
|
|
const options = opts || {};
|
|
const argv = options.argv || process.argv;
|
|
const env = options.env || process.env;
|
|
const index = argv.indexOf(DEBUG_LOG_ARG);
|
|
if (index !== -1) {
|
|
const next = argv[index + 1];
|
|
if (typeof next === 'string' && next.length > 0 && next.indexOf('--') !== 0)
|
|
return next;
|
|
return null;
|
|
}
|
|
const fromEnv = env[DEBUG_LOG_ENV];
|
|
if (typeof fromEnv === 'string' && fromEnv.length > 0) return fromEnv;
|
|
return null;
|
|
}
|
|
|
|
function toRecord(source, level, event, data) {
|
|
const ts = Date.now();
|
|
let iso = '';
|
|
try {
|
|
iso = new Date(ts).toISOString();
|
|
} catch (error) {
|
|
iso = '';
|
|
}
|
|
return {
|
|
ts,
|
|
iso,
|
|
source,
|
|
level,
|
|
event,
|
|
data: data && typeof data === 'object' ? data : {},
|
|
};
|
|
}
|
|
|
|
function makeDisabledLogger(logPath, cause) {
|
|
function noop() {
|
|
return null;
|
|
}
|
|
return {
|
|
enabled: false,
|
|
logPath: logPath || null,
|
|
openError: cause,
|
|
log: noop,
|
|
debug: noop,
|
|
info: noop,
|
|
warn: noop,
|
|
error: noop,
|
|
close: () => {},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Open the JSONL appender. `logPath` missing/unwritable (absent parent
|
|
* dir, a directory, permissions) yields a disabled logger — never throws.
|
|
*/
|
|
function createDebugLogger(opts) {
|
|
const options = opts || {};
|
|
const logPath = options.logPath;
|
|
const source = options.source || 'main';
|
|
if (typeof logPath !== 'string' || logPath.length === 0) {
|
|
return makeDisabledLogger(
|
|
null,
|
|
new Error('[heretek-debug] no log path configured')
|
|
);
|
|
}
|
|
try {
|
|
// Eager probe: creates the file when missing, proves writability now
|
|
// instead of dropping the first real event later.
|
|
fs.appendFileSync(logPath, '');
|
|
} catch (error) {
|
|
return makeDisabledLogger(logPath, error);
|
|
}
|
|
function log(level, event, data, customSource) {
|
|
let record;
|
|
try {
|
|
record = toRecord(customSource || source, level, event, data);
|
|
fs.appendFileSync(logPath, JSON.stringify(record) + '\n');
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
return record;
|
|
}
|
|
return {
|
|
enabled: true,
|
|
logPath,
|
|
openError: null,
|
|
log,
|
|
debug: (event, data, customSource) => log('debug', event, data, customSource),
|
|
info: (event, data, customSource) => log('info', event, data, customSource),
|
|
warn: (event, data, customSource) => log('warn', event, data, customSource),
|
|
error: (event, data, customSource) => log('error', event, data, customSource),
|
|
close: () => {},
|
|
};
|
|
}
|
|
|
|
/** Parse one JSONL line; null when malformed (callers count and skip). */
|
|
function parseDebugLogLine(line) {
|
|
if (!line || typeof line !== 'string') return null;
|
|
const trimmed = line.trim();
|
|
if (trimmed.length === 0) return null;
|
|
try {
|
|
const record = JSON.parse(trimmed);
|
|
if (!record || typeof record !== 'object') return null;
|
|
return record;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
DEBUG_LOG_ARG,
|
|
DEBUG_LOG_ENV,
|
|
LEVELS,
|
|
resolveDebugLogPath,
|
|
createDebugLogger,
|
|
parseDebugLogLine,
|
|
};
|