Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a local diagnostic trail for OpenCode skill tracking, writing diagnostic states to a separate JSONL file to ensure durability when database writes fail, and adds a CLI to view database status, recent calls, and diagnostics. The reviewer identified several key issues, including a race condition and performance bottleneck during diagnostic logging, a potential crash when reading corrupted JSONL lines, unsafe environment variable parsing for the recent calls limit, and an ineffective asynchronous database disconnection registered on the synchronous process exit hook.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export async function appendDiagnosticEvent(event: SkillDiagnosticEvent): Promise<void> { | ||
| const filePath = getDiagnosticsPath(); | ||
| await fs.mkdir(path.dirname(filePath), { recursive: true }); | ||
|
|
||
| const existing = await readRecentDiagnosticEvents(getDiagnosticLimit() - 1); | ||
| const entries = [...existing.reverse(), event].slice(-getDiagnosticLimit()); | ||
| const lines = entries.map((entry) => JSON.stringify(entry)).join('\n'); | ||
| await fs.writeFile(filePath, `${lines}\n`, 'utf8'); | ||
| } |
There was a problem hiding this comment.
Race Condition & Performance Bottleneck
Reading, parsing, reversing, and rewriting the entire diagnostics file on every single diagnostic event introduces a significant performance bottleneck and a race condition:
- I/O Overhead: For every successful skill execution, three events (
received,normalized,stored) are logged. This results in 3 full reads and 3 full writes of the JSONL file per skill call. As the log grows, this will cause high disk I/O and slow down skill execution. - Race Condition: If multiple skill calls run concurrently, their read-modify-write cycles will overlap, leading to lost events or file corruption.
Recommendation
We can serialize the write operations using a simple promise queue to prevent concurrent read-modify-write race conditions.
let writeQueue = Promise.resolve();
export async function appendDiagnosticEvent(event: SkillDiagnosticEvent): Promise<void> {
const filePath = getDiagnosticsPath();
await fs.mkdir(path.dirname(filePath), { recursive: true });
writeQueue = writeQueue.then(async () => {
const existing = await readRecentDiagnosticEvents(getDiagnosticLimit() - 1);
const entries = [...existing.reverse(), event].slice(-getDiagnosticLimit());
const lines = entries.map((entry) => JSON.stringify(entry)).join('\n');
await fs.writeFile(filePath, `${lines}\n`, 'utf8');
}).catch(() => {});
await writeQueue;
}| export async function readRecentDiagnosticEvents(limit: number): Promise<SkillDiagnosticEvent[]> { | ||
| const filePath = getDiagnosticsPath(); | ||
|
|
||
| try { | ||
| const contents = await fs.readFile(filePath, 'utf8'); | ||
| return contents | ||
| .split('\n') | ||
| .filter(Boolean) | ||
| .map((line) => JSON.parse(line) as SkillDiagnosticEvent) | ||
| .slice(-limit) | ||
| .reverse(); | ||
| } catch (error) { | ||
| const code = (error as NodeJS.ErrnoException).code; | ||
| if (code === 'ENOENT') { | ||
| return []; | ||
| } | ||
|
|
||
| throw error; | ||
| } | ||
| } |
There was a problem hiding this comment.
Potential Crash on Corrupted Log Lines
If the JSONL file contains any partially written or corrupted lines (which can happen due to unexpected process termination or concurrent writes), JSON.parse(line) will throw an error and crash the entire diagnostics reading flow.
Recommendation
Wrap JSON.parse in a try-catch block to gracefully skip any corrupted lines and ensure the diagnostics reader remains resilient.
export async function readRecentDiagnosticEvents(limit: number): Promise<SkillDiagnosticEvent[]> {
const filePath = getDiagnosticsPath();
try {
const contents = await fs.readFile(filePath, 'utf8');
const events: SkillDiagnosticEvent[] = [];
const lines = contents.split('\n').filter(Boolean);
for (const line of lines) {
try {
events.push(JSON.parse(line) as SkillDiagnosticEvent);
} catch {
// Ignore corrupted or partially written lines
}
}
return events.slice(-limit).reverse();
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === 'ENOENT') {
return [];
}
throw error;
}
}| export async function runRecentDiagnostics(limit = Number(process.env.TRACKER_RECENT_LIMIT ?? 10)): Promise<void> { | ||
| const recent = await readRecentDiagnosticEvents(limit); |
There was a problem hiding this comment.
Unsafe Environment Variable Parsing
If process.env.TRACKER_RECENT_LIMIT is set to an invalid non-numeric string, Number(...) will return NaN, which could cause unexpected behavior when passed to downstream functions.
Recommendation
Parse the environment variable safely with a fallback to the default value.
| export async function runRecentDiagnostics(limit = Number(process.env.TRACKER_RECENT_LIMIT ?? 10)): Promise<void> { | |
| const recent = await readRecentDiagnosticEvents(limit); | |
| export async function runRecentDiagnostics(limit?: number): Promise<void> { | |
| const rawLimit = process.env.TRACKER_RECENT_LIMIT; | |
| const resolvedLimit = limit ?? (rawLimit ? (parseInt(rawLimit, 10) || 10) : 10); | |
| const recent = await readRecentDiagnosticEvents(resolvedLimit); |
| process.on('exit', () => { | ||
| void prisma.$disconnect(); | ||
| }); |
There was a problem hiding this comment.
Asynchronous Cleanup on Process Exit
The exit event in Node.js does not support asynchronous operations:
Listener functions must perform only synchronous operations. The Node.js process will exit immediately after calling the 'exit' event listeners causing any additional work still queued in the event loop to be abandoned.
Since prisma.$disconnect() returns a Promise and performs asynchronous network/file I/O, it will be cut off and won't complete successfully.
Recommendation
Either remove the process.on('exit') listener entirely (as Node.js and the OS automatically close open file descriptors and database connections upon termination), or use process.on('beforeExit') if you need to perform asynchronous cleanup.
| export async function runRecentCalls(limit = Number(process.env.TRACKER_RECENT_LIMIT ?? 10)): Promise<void> { | ||
| const prisma = getPrismaClient(); | ||
|
|
||
| try { | ||
| const calls = await prisma.skillCall.findMany({ | ||
| orderBy: { timestamp: 'desc' }, | ||
| take: limit, | ||
| }); |
There was a problem hiding this comment.
Unsafe Environment Variable Parsing
If process.env.TRACKER_RECENT_LIMIT is set to an invalid non-numeric string, Number(...) will return NaN, which will cause prisma.skillCall.findMany to throw an error or fail.
Recommendation
Parse the environment variable safely with a fallback to the default value.
| export async function runRecentCalls(limit = Number(process.env.TRACKER_RECENT_LIMIT ?? 10)): Promise<void> { | |
| const prisma = getPrismaClient(); | |
| try { | |
| const calls = await prisma.skillCall.findMany({ | |
| orderBy: { timestamp: 'desc' }, | |
| take: limit, | |
| }); | |
| export async function runRecentCalls(limit?: number): Promise<void> { | |
| const rawLimit = process.env.TRACKER_RECENT_LIMIT; | |
| const resolvedLimit = limit ?? (rawLimit ? (parseInt(rawLimit, 10) || 10) : 10); | |
| const prisma = getPrismaClient(); | |
| try { | |
| const calls = await prisma.skillCall.findMany({ | |
| orderBy: { timestamp: 'desc' }, | |
| take: resolvedLimit, | |
| }); |
No description provided.