npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

opencode-keepalive

v0.1.0

Published

OpenCode plugin that keeps sessions alive: emits liveness heartbeats and recovers stalled agents

Readme

Opencode Keepalive

OpenCode plugin that keeps sessions alive: emits liveness heartbeats for external watchdogs and detects (and optionally recovers) stalled agents.


Getting Started

opencode-keepalive is a "watchdog" plugin for OpenCode. It monitors your agent's activity and helps ensure work doesn't grind to a halt without you knowing.

  • Liveness Log: Emits continuous JSON telemetry to a background log file.
  • Stall Detection: Automatically detects when an agent is "busy" but has stopped making progress.
  • Auto-Recovery: Can automatically unstick stalled subagents by aborting them and nudging the parent agent to resume.

Installation

npm install opencode-keepalive

Enable it in your opencode.json (or opencode.jsonc):

{
  "plugins": [
    ["opencode-keepalive", {
      "childStallAction": "abort-resume-parent"
    }]
  ]
}

Quick Start

By default, the plugin works out of the box with zero configuration. It appends NDJSON (Newline Delimited JSON) heartbeats to:

~/.local/state/opencode-keepalive/heartbeat.ndjson
# or $XDG_STATE_HOME/opencode-keepalive/heartbeat.ndjson

You can view the pulse of your agent in real-time:

tail -f ~/.local/state/opencode-keepalive/heartbeat.ndjson | jq .

Common Recipes

1. Pipe to an external watchdog If you are using a process manager like Harbor that expects heartbeats on stdout:

["opencode-keepalive", { "output": "stdout" }]

2. Enable automatic subagent recovery To have the plugin automatically abort hung subagents and ask the parent to retry:

["opencode-keepalive", { "childStallAction": "abort-resume-parent" }]

3. Silence all output To keep the stall detection logic running but stop writing to any log:

["opencode-keepalive", { "output": "none" }]

Configuration Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | intervalMs | number | 10000 | Interval between standard heartbeats (ms). | | includeDetails | boolean | true | Include event types, tool names, and stall details. | | includeSessionID | boolean | true | Include session IDs in heartbeats. | | stallMs | number | 120000 | Duration of no-progress before triggering a stall (ms). | | childStallAction | string | "report" | Action for stalled children: "report" or "abort-resume-parent". | | maxStallRecoveries | number | 3 | Max recovery attempts per child session. | | abortWaitMs | number | 30000 | Wait time after abort before nudging parent (ms). | | toolStallMsByTool | object | {...} | Per-tool stall budgets (e.g., {"glob": 60000}). | | bashQuickStallMs | number | 60000 | Stall budget for "quick" bash commands (ls, cat, etc.). | | resumeMessageTemplate| string | ... | Template used for parent nudge messages. | | output | string | "file" | Where to write: "file", "stdout", "stderr", or "none". | | logFile | string | ... | Custom path when output is "file". |

Troubleshooting

  • No heartbeats appearing? Check the logFile path. If you are on a non-standard system, ensure XDG_STATE_HOME is set or provide an explicit logFile path.
  • False positive stalls? If a long command (like a large git clone) is stalling, it might be classified incorrectly. You can unmap bash stall budgets by setting toolStallMsByTool: { "bash": null }.
  • Too much log noise? Increase intervalMs to reduce the frequency of keepalive pulses.

How It Works

Event Model

The plugin emits envelopes with type: "heartbeat". The behavior is driven by 7 distinct triggers:

| Trigger | When it fires | |---------|---------------| | event | Any OpenCode event (except other heartbeats). | | idle | Periodic pulse when the system is completely idle. | | tool.execute.before | Immediately before a tool starts running. | | tool.execute.after | Immediately after a tool finishes. | | tool.running | Periodic "keepalive" pulse while a tool is in-flight. | | stall | Fired when the stall budget is exceeded. | | stall.recovery | Fired during an automatic recovery attempt (or error). |

Example Envelope:

{
  "type": "heartbeat",
  "properties": {
    "source": "opencode-keepalive",
    "timestamp": "2026-07-28T12:00:00.000Z",
    "unixMs": 1785240000000,
    "trigger": "stall",
    "sessionID": "sess_123",
    "parentID": "sess_000",
    "stalledForMs": 120000
  }
}

Emission Internals

  1. Throttling: Standard events are batched. If an event occurs within intervalMs of the last heartbeat, it is held in a "pending" slot and flushed later. stall and stall.recovery triggers bypass this throttle for immediate visibility.
  2. Keepalive Timer: While a tool is active, a dedicated timer re-emits the last tool.running signal at the intervalMs frequency. Crucially, these keepalives do NOT reset the stall clock.
  3. Backpressure: When writing to stdout or stderr, the plugin respects stream drain signals to avoid memory bloat during heavy event volume.

Stall Detection Model

A session is considered stalled if its status is busy but no progress has been noted for the duration of its effective budget.

  • Progress is: Message updates (part.updated, part.delta), tool lifecycle events (before, after), or command events.
  • Progress is NOT: Internal heartbeats or tool.running keepalives.

Effective Budgets

The plugin uses a "strictest fuse" rule:

  1. No tools in flight: Uses global stallMs.
  2. Mapped tools in flight: If tools like glob or grep are running, the minimum of their mapped budgets is used (default 60s).
  3. Unmapped tools only: If only unmapped tools (like a long npm install) are running, the short-fuse timer is disabled entirely to avoid false positives.

Bash Classification

The bash tool is special. The plugin inspects the first segment of the command to classify it:

  • Quick: cat, ls, grep, jq, mkdir, rm, etc. These use bashQuickStallMs.
  • Long: npm, docker, git, python, tsc, vitest, etc. These are unmapped by default.
  • Unknown: Fail-safe to Long.

Subagent Recovery

When childStallAction is "abort-resume-parent":

  1. stall.recovery is emitted.
  2. The plugin calls session.abort(childID).
  3. It waits for the child to become idle (up to abortWaitMs).
  4. It sends a synthetic prompt to the parent session with the task_id of the stalled child, allowing the parent to naturally resume the work.

Architecture Tour

| Module | Responsibility | |--------|----------------| | index.ts | Entry point; wires options to state and hooks. | | hooks.ts | OpenCode lifecycle hooks; translates events to heartbeats/progress. | | heartbeat.ts | The core emission engine, throttler, and timers. | | sessions.ts | State tracking for active sessions and progress timestamps. | | tools.ts | Tool name normalization and effective budget calculation. | | bash-command.ts | Logic for classifying bash commands as quick vs long. | | recover.ts | Implementation of the abort-resume-parent sequence. | | state.ts | Runtime state and IO abstraction (the test seam). |

Edge Cases

  • Unmapped Silence: If an agent is running an unmapped tool (like sleep 300) and the model is silent, the plugin will not stall. It relies on the external worker wall timeout.
  • Waiting for User: When an agent is blocked on a permission.ask dialog, the stall timer is automatically disarmed to prevent "stalling" while waiting for you.

License

MIT