Skip to content

feat: add skill diagnostics verification flow - #1

Open
DKER2 wants to merge 3 commits into
masterfrom
feat/skill-diagnostics-v1
Open

DKER2 wants to merge 3 commits into
masterfrom
feat/skill-diagnostics-v1

Conversation

@DKER2

@DKER2 DKER2 commented Jun 27, 2026

Copy link
Copy Markdown
Owner

No description provided.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/diagnostic-log.ts
Comment on lines +32 to +40
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');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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:

  1. 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.
  2. 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;
}

Comment thread src/diagnostic-log.ts
Comment on lines +42 to +61
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;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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;
  }
}

Comment thread src/diagnostic-log.ts Outdated
Comment on lines +63 to +64
export async function runRecentDiagnostics(limit = Number(process.env.TRACKER_RECENT_LIMIT ?? 10)): Promise<void> {
const recent = await readRecentDiagnosticEvents(limit);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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);

Comment thread src/opencode-plugin.ts Outdated
Comment on lines +66 to +68
process.on('exit', () => {
void prisma.$disconnect();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Comment thread src/recent-calls.ts Outdated
Comment on lines +3 to +10
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,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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,
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant