Bug Description
When opencode's AI agent performs batch file edits (e.g., multiedit, patch, or rapid sequential edits), multiple wakatime-cli processes are spawned but never exit. These zombie processes accumulate, each consuming ~1.6GB of physical memory and spinning at high CPU.
Environment
- opencode-wakatime version: 1.3.0
- wakatime-cli version: v2.13.0
- opencode version: 1.14.39
- OS: macOS Darwin 26.3.1 (arm64)
Reproduction Steps
- Start opencode with the wakatime plugin enabled
- Have the AI agent perform batch file edits (e.g., a multiedit across 7+ files)
- Observe that wakatime-cli processes are spawned but never exit
Observed Behavior
- 7+ wakatime-cli processes stuck in running state (R+) for 6+ minutes
- Each process: ~1.6GB physical memory (peak 1.9GB), 23-183% CPU
- Total memory: ~11GB leaked
- Processes are children of opencode (PID 16448), communicating via unix pipes
- Parent process stops reading from pipes, but child processes never exit
Root Cause Analysis
Issue 1: No timeout or kill mechanism in sendHeartbeat()
In src/wakatime.ts, the sendHeartbeat function uses execFile to spawn wakatime-cli:
export function sendHeartbeat(params: HeartbeatParams): Promise<void> {
return new Promise((resolve) => {
execFile(cliLocation, args, execOptions, (error, stdout, stderr) => {
resolve();
});
});
}
The Promise only resolves when the child process exits. If wakatime-cli hangs, the Promise never resolves and the process leaks. There is no timeout, no child.kill(), and no cleanup mechanism.
Issue 2: Fire-and-forget heartbeats
In src/index.ts, processHeartbeat() is called after every tool execution:
// When force=false (default), promises are NOT awaited
const promise = sendHeartbeat({...});
if (force) {
heartbeatPromises.push(promise);
}
When force=false (the normal case), heartbeat promises are fired and forgotten. If the child process hangs, it leaks. The fileChanges map is cleared immediately, so there's no way to detect or recover from stuck processes.
Issue 3: --project-folder / points to root directory
The command line shows --project-folder / which points to the filesystem root. This may cause wakatime-cli to recursively scan the entire filesystem, contributing to the 1.6GB memory usage and hang.
Process Sample
sample output shows the main thread stuck in kqueue event loop:
- Main thread:
kevent busy-waiting
- Another thread:
pthread_cond_wait + pthread_kill cycling
- This is a Go runtime goroutine leak / busy-wait pattern
Expected Behavior
- wakatime-cli processes should exit within seconds
- If a process hangs, it should be killed after a timeout (e.g., 30s)
- Orphaned processes should be cleaned up on plugin shutdown
Suggested Fix
-
Add a timeout to sendHeartbeat() (e.g., 30 seconds):
const timeout = setTimeout(() => {
child.kill(SIGTERM);
resolve(); // Don't let a hung process block forever
}, 30000);
-
Track spawned child processes and kill them on shutdown
-
Consider using spawn with stdio: ignore and detached: true so the child is independent and doesn't hold pipes open
-
Fix the --project-folder argument to point to the actual project directory, not /
Bug Description
When opencode's AI agent performs batch file edits (e.g., multiedit, patch, or rapid sequential edits), multiple
wakatime-cliprocesses are spawned but never exit. These zombie processes accumulate, each consuming ~1.6GB of physical memory and spinning at high CPU.Environment
Reproduction Steps
Observed Behavior
Root Cause Analysis
Issue 1: No timeout or kill mechanism in sendHeartbeat()
In
src/wakatime.ts, thesendHeartbeatfunction usesexecFileto spawn wakatime-cli:The Promise only resolves when the child process exits. If wakatime-cli hangs, the Promise never resolves and the process leaks. There is no timeout, no
child.kill(), and no cleanup mechanism.Issue 2: Fire-and-forget heartbeats
In
src/index.ts,processHeartbeat()is called after every tool execution:When
force=false(the normal case), heartbeat promises are fired and forgotten. If the child process hangs, it leaks. ThefileChangesmap is cleared immediately, so there's no way to detect or recover from stuck processes.Issue 3: --project-folder / points to root directory
The command line shows
--project-folder /which points to the filesystem root. This may cause wakatime-cli to recursively scan the entire filesystem, contributing to the 1.6GB memory usage and hang.Process Sample
sampleoutput shows the main thread stuck in kqueue event loop:keventbusy-waitingpthread_cond_wait+pthread_killcyclingExpected Behavior
Suggested Fix
Add a timeout to
sendHeartbeat()(e.g., 30 seconds):Track spawned child processes and kill them on shutdown
Consider using
spawnwithstdio: ignoreanddetached: trueso the child is independent and doesn't hold pipes openFix the
--project-folderargument to point to the actual project directory, not/