All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Discord presence reset to "$0 spent" when the AI finished. A
session in the
"Waiting for command"state (shown as "Completed!" in the user's config) was treated as idle, so the renderer picked theidletemplate set whosedetailshardcoded• $0 spent. The accumulated cost was never reset (it stayed correct on the session), only the displayed text reset to $0. Root cause:src/plugin/local-presence.jscomputedisIdle = !session || session.state === "Waiting for command", which short-circuitedbyState["Waiting for command"](the user's "Completed!" + dynamic{costCompact}template) into dead code. Fix: only a truly absent session (queue empty, nothing displayed) uses theidletemplate set. A session in"Waiting for command"now renders throughchooseTemplates(config.templates, state), sobyState["Waiting for command"]takes effect and the real cost (e.g.$0.26 spent) persists after the AI finishes. byState["Waiting for command"]template is now honored. It was previously unreachable because the WAITING state fell through to theidleset. Users who configured a "Completed!" state text or a dynamic cost expression there will now see it.
tests/template-selection.mjs: 13-assertion regression unit suite covering template selection for every state. Guards against reintroducing the WAITING-as-idle conflation:- WAITING session renders dynamic cost, not hardcoded
$0 spent. - WAITING session with zero cost still renders
$0 spent. - Null session (no session at all) uses the
idletemplate set. - Working / Thinking / Typing / Asking still use their
byStatetemplates. - WAITING selects
byStateeven when anidletemplate exists.
- WAITING session renders dynamic cost, not hardcoded
npm run test:template-selectionscript;npm testnow runs it after phase2-v2 and before cli-lifecycle.
src/plugin/local-presence.js:renderPresencenow computesisIdle = !session. Documentation comment added explaining the WAITING vs idle distinction.src/cli/help.jsversion references bumped to v3.1.9.
.github/workflows/post-release.yml: post-release user simulation. Triggers onrelease: types: [published], downloads the REAL published tarball from GitHub Releases, and runs the full install + update + uninstall flow including upgrade from the previous release,update --stable,update --dev, and bad-ref-doesn't-clobber. This is the only place curl-to-GitHub runs in the test suite (from CI IP, not the developer's). Failures here do NOT retract the release (it is already public); they surface as a failed job so the maintainer can decide whether to cut a hotfix.tests/post-release.mjs: post-release user simulation harness. 10 assertions covering real-tarball install, version, help, upgrade from previous release, invalid-ref-doesn't-clobber,update --stable,update --dev, and final uninstall. Can be triggered locally withORP_POST_RELEASE_FORCE=1(requires a published release matching the version in package.json).- Test distribution matrix in
AGENTS.md: documents which tests live in which workflow (commit-time / pre-release gate / post-release user simulation / manual checklist), with explicit curl discipline rule (npm testMUST NOT curl GitHub by default). - Manual test checklist in
AGENTS.md: M1-M6 scenarios CI cannot cover (Discord IPC, OpenCode integration, multi-window handoff, Windows/macOS platform, real network conditions).
tests/release-smoke.mjsrenamed totests/pre-release.mjs. Now the explicit pre-release gate that runs betweennpm packandCreate GitHub releasein the release workflow. Uses the local tarball (no curl), covers install + uninstall + idempotent re-install + tarball contents + package.json sanity. ~10 assertions.tests/cli-lifecycle.mjssection 3 default changed: now defaults tonpm pack(no GitHub curl) sonpm testis safe to run repeatedly from a developer's local machine. SetORP_USE_GITHUB_RELEASE=1to opt in to the GitHub download path (rare; only for debugging publish-specific issues)..github/workflows/release.yml: replacesnpm run test:release-smokewithnpm run test:pre-release -- --tarball=<built-tarball>. The release workflow now runs:npm test(commit-time tests: phase1+2+2-v2+cli-lifecycle)- Smoke (help/version)
npm pack+ rename- Pre-release gate (fail-fast on broken tarball)
- Publish to npm (optional)
- Create GitHub release
- npm scripts (
package.json):test:release-smokerenamed totest:pre-release, addedtest:post-release.
- HTTP 429 risk on local
npm test: cli-lifecycle.mjs §3 no longer unconditionally curl-downloads from GitHub Releases. Local runs usenpm pack(no network). This removes the developer's IP from being flagged by GitHub's anti-scraping rate limiter.
-
tests/cli-lifecycle.mjs: commit-time CLI lifecycle regression suite (67 assertions, 3 sections). Verifies CLI output, arg validation,install.shunit checks, and end-to-end install in an isolated npm prefix. Falls back tonpm pack(local tarball) when the current version is not yet published. -
tests/release-smoke.mjs: release-time smoke test (~10 assertions). Verifies the just-built tarball installs cleanly, thatupdate --refcan upgrade from the previous release, and that an invalid--refdoes not clobber the install. Runs ONLY in the release workflow after the tarball is built but before the GitHub release is published.Note:
update --stableandupdate --devare covered bytests/cli-lifecycle.mjs(commit-time, runs against local source) rather than here, because at release-smoke time the current version is not yet published to the GitHub Releases API so--stablewould query the previous release.Total assertions across all 5 harnesses when both run (commit-time + release-time): 157.
npm test(commit-time): now runs phase1 + phase2 + phase2-v2 + cli-lifecycle (commit-time portion). Release-smoke is invoked separately in the release workflow.npm run test:release-smoke: new script that invokestests/release-smoke.mjs.tests/cli-lifecycle.mjs: removed Section 4 (update flow). Section 4 requires a published release and moved totests/release-smoke.mjs..github/workflows/release.yml: now runsnpm run test:release-smokeafternpm packbut before the GitHub release is created. A failing smoke test fails the workflow (no release published)..github/workflows/release.yml: replaced stale release body that recommendednpm install -g <repo>#<tag>and referenced the mergedredesign/v3-daemonbranch. New body leads with the curl installer and explains the npm v11 bug..github/workflows/test.yml: removedredesign/v3-daemonfrom branch triggers (merged into main).- v3.1.5 GitHub Release body shortened to a "superseded" note.
The release page previously contained a long install guide that
included the broken
npm install -g <repo>#<ref>pattern. The tag and tarball asset are preserved (for reproducibility) but the install guidance now lives on the v3.1.6 release page.
-
opencode-rpc helpno longer recommends the broken npm v11 git-dep install pattern. v3.1.5's help text includednpm install -g Khip01/opencode-rich-presenceas a "stable release" install option. That command is broken on npm v11: npm reports "added 1 package" but never creates theopencode-rpcbinary, leaving the user withzsh: command not found: opencode-rpc. Replaced with the curl installer and the manual tarball install, both of which work. -
Removed stale "main is v2.1.1 (pre-redesign)" and "redesign/v3-daemon" references from
src/cli/help.jsandsrc/cli/update.js. Theredesign/v3-daemonbranch was merged intomainin v3.0.0-phase2; currentmainis v3.1.6.
-
install.shone-liner installer. A bash script at the repo root that downloads the latest stable tarball from GitHub Releases and runsnpm install -g <tarball>+opencode-rpc install. Closes the fresh-install gap that the npm v11 git-dep bug creates: previously,opencode-rpc update --ref <tag>was the recommended path, but it requiredopencode-rpcto already be on PATH, which is impossible on a fresh machine.Supports Linux, macOS, and Windows (via Git Bash / MSYS2 / Cygwin / WSL). Pure cmd.exe / PowerShell users should use the manual tarball install path documented in
docs/INSTALL.md.Usage:
curl -fsSL https://raw.githubusercontent.com/Khip01/opencode-rich-presence/main/install.sh | bashPin to a specific version with
ORP_VERSION=vX.Y.Z.
- README.md and docs/INSTALL.md updated to recommend the curl
installer and tarball install as the primary install paths.
npm install -g <repo>#<ref>is documented as broken on npm v11 with a clear explanation of why the package cannot work around it. The curl installer is presented as supported on Windows via Git Bash / MSYS2 / Cygwin / WSL.
-
secretscontext in step-levelif:(workflow YAML broken). GitHub Actions step-levelif:does not support thesecretscontext directly. Usingif: ${{ secrets.NPM_TOKEN != '' }}orif: secrets.NPM_TOKEN != ''both fail with "Unrecognized named-value: secrets". Fixed by passing the secret to anenv:variable (NPM_TOKEN_CHECK: ${{ secrets.NPM_TOKEN }}) and checkingif: env.NPM_TOKEN_CHECK != ''. -
!operator in job-levelif:caused workflow parser failure. The!(negation) operator is reserved YAML notation and must be wrapped in${{ }}at the job level:if: ${{ !contains(...) }}. -
Tag filter pattern
[0-9]+.*was regex, not glob. GitHub Actions uses Fnmatch/glob patterns for tag filters, not regex. Changed to[0-9]*(standard glob for digit-starting tags). -
Missing
registry-urlinsetup-nodestep. Required fornpm publishto authenticate viaNODE_AUTH_TOKEN. -
Missing
id-token: writepermission. Required fornpm publish --provenanceOIDC token generation. -
Test harness isolation via
test-env.mjs. Harnesses no longer use the user's real~/.config/opencodedirectory; each run gets a freshmkdtempSynctemp directory. -
Daemon logs push intent unconditionally.
pushCurrentPresenceandclearPresencenow log their decision even when Discord is not connected, so CI environments without Discord can verify the daemon's push logic. -
Assertion string updates for Typing template and asset key. The Typing template renders as
<model> · Typing(Typing followed by closing quote), and the default asset key was changed toopencode-logo-too-opencode-rpc.
- GitHub Actions
test.ymlruns full test matrix on Node 20/22/24 (Ubuntu) on push/PR tomainorredesign/v3-daemon. release.ymlbuilds tarball + creates GitHub Release on tag push, with optional npm publish (requiresNPM_TOKENsecret).
Phase 2 of the v3 redesign. Adds the daemon that holds a single Discord IPC connection for the whole machine. All OpenCode plugin instances connect to it via local Unix socket, send their session state, and the daemon pushes to Discord in place via SET_ACTIVITY. Handoff between OpenCode terminals no longer disconnects from Discord (the connection stays open in the daemon).
User reported two issues after daily use:
- Display stuck on "Typing" for fast AI responses. When the AI answers in under 4s (sub-throttle window), the WORKING push lands but the TYPING and WAITING pushes get throttled. Display stayed on the last landed state (often Typing). Fix: when a push is throttled, schedule a delayed push for the LATEST state. The timer is re-armed on each new state so we always push the most recent payload, not a stale intermediate. After the 4s throttle window, the delayed push fires and the final state (e.g. WAITING) always lands on Discord.
- Reopening OpenCode after all-instances-exit did not show
presence. Daemon exited 2s after the last instance's goodbye.
If the user reopened OpenCode more than 2s later, a new daemon
had to spawn and reconnect to Discord, hitting a cooldown window
that required
opencode-rpc restart. Fix: extendEXIT_GRACE_MSfrom 2s to 10s. If a new instance connects during the grace window, cancel the exit timer so the daemon stays alive with its existing Discord connection. The /exit-all behavior is preserved (daemon still clears presence and exits, just after a longer window when no new clients appear). - Reopening OpenCode after /exit-all did not show presence at all,
even with
opencode-rpc restart. The 10s grace from fix #2 was not enough: a Discord App-ID cooldown window could block the reconnect entirely for tens of seconds, andopencode-rpc restartdid not help because it just respawned the daemon (still blocked by the cooldown). Fix: the daemon no longer auto-exits. When the last client disconnects, it sendsclearActivityto Discord (display clears, /exit-all UX preserved) then STAYS ALIVE idle. The next OpenCode launch connects to the existing daemon and reuses its already-open Discord connection. No reconnect, no cooldown exposure, display appears instantly on next firing. Daemon resource cost when idle: ~30MB RAM, ~0% CPU. Termination is now explicit: SIGINT/SIGTERM (fromopencode-rpc restartor a manual kill). - Daemon could not detect a silently-dead Discord socket. When Discord Desktop was killed or restarted while the daemon held the IPC fd, the OS did not surface close/error on our end. The daemon kept calling setActivity but no frame ever reached Discord. Display stayed blank with no obvious error and no automatic recovery. Fix: the daemon now pings Discord every 15s (opcode 3) and tracks pong replies (opcode 4). If no pong arrives within 30s, the connection is treated as dead and the standard reconnect path is triggered. Also: write errors on the IPC socket now propagate to the disconnected handler immediately (previously a silently-dead socket buffered writes without flagging an error).
- Display sometimes did not refresh after all-exit + reopen even when the daemon was pushing successfully. Theory: Discord internally throttles / drops updates for App IDs that go through a clearActivity + idle pattern, and the next setActivity is ignored. Fix: when a new instance connects (hello), the daemon forces a refresh by sending clearActivity to wipe Discord's internal display state. The fingerprint is reset so the new instance's first setActivity actually fires.
- First fire after reopen-during-cooldown window still did not show
display. The hello-time refresh from fix #5 introduced a new bug:
after closing the last instance, the goodbye handler already sends
clearActivity. The next hello from a reopened instance then sends
another clearActivity immediately followed by SET_ACTIVITY. Discord
silently drops SET_ACTIVITY sent within ~1-2s of clearActivity on
the same IPC connection. The display stayed blank until a second
instance fired (whose clearActivity + SET_ACTIVITY pair landed
after the cooldown window). Fix attempt (aedfa2d): track
lastClearSentAtand skip the hello refresh if we cleared within the last 60s. This worked for the "old daemon still alive" case but not for the "daemon died and got respawned" case (the new daemon haslastClearSentAt = 0, so the refresh still fires). Fix (92ef569): remove the hello-time refresh clearActivity entirely. Trust SET_ACTIVITY alone. Discord reliably accepts SET_ACTIVITY when there is no preceding clearActivity on the same connection. Recovery for stuck displays after a very long idle (genuine stale Discord state, App-ID rate-limit survival longer than the daemon's lifetime):opencode-rpc restart. This path is rarer than the false-positive refresh-drop bug, so we trade it for simpler, reliable behavior. - Daemon died silently between cycles even after removing refresh
clearActivity. After fix #6 the bug still reproduced: close all
opencode (daemon cleared, stays alive), open one new opencode,
fire chat.message. The new instance's
hellowas logged (instance registered: pid=...) but nopush pid=...log followed, noexit/SIGTERM/fatallog either, and the daemon socket disappeared within seconds. First fix attempt (05e99de): add.catch()to fire-and-forgetpushCurrentPresencecalls and adduncaughtException/unhandledRejectionhandlers. That made the next reproduction emit a stack trace and pinpointed the exact cause:EPIPEonprocess.stderr.writeinsidelogToFile. The daemon was spawned withstdio: ["ignore", "ignore", "pipe"]; the stderr pipe was connected to the parent plugin. When the parent opencode exited, the pipe closed. Every subsequent daemon stderr write (including theinstance registered: ...log) threw EPIPE, which became an uncaughtException, which terminated the daemon with exit code 1. The fix attempts after that hid the crash log but did not stop the crash. Definitive fix (current): catch EPIPE explicitly inlogToFile(do not re-throw) and change daemon-spawner.js to usestdio: ["ignore", "ignore", "ignore"]so no stderr pipe is created in the first place. Daemon logs already go to the activity log viaappendFileSync, so stderr is redundant. opencode-rpc update --ref <bad-ref>deletes the existing CLI. Reordering fix:runNpmInstallpreviously calledcleanExistingInstall()at the start, before attempting the git fetch. If the fetch failed (typo in ref, network blip, the ref did not exist), the old install was already gone and the user was left with no working CLI command. Move the cleanup to AFTERnpm packsucceeds: build the tarball first, only then remove the old install. If anything before the tarball build fails, the old install stays untouched. Also reject--refvalues containing whitespace or control characters at argument parse time, before any work begins. Patch bump 3.1.0 -> 3.1.1.- GitHub Actions CI runs the full test matrix on every push and
PR. Previously the test workflow ran only smoke checks
(
help,version,info,npm pack --dry-run) and never executed the actual regression harnesses. Now test.yml runs the full Phase 1 + Phase 2 + Phase 2 v2 harnesses on Node 20, 22, 24 (Ubuntu). Triggers: push or PR tomainorredesign/v3-daemon, plus manualworkflow_dispatch. Concurrency-cancelled on new pushes to the same ref so fast pushes do not queue redundant runs. Thepackage.jsontestscript now runs all three harnesses sequentially, sonpm testmatches what CI does. - GitHub Actions release workflow updated. release.yml now
also accepts non-
v-prefixed tags (e.g.3.1.2-phase2) so pre-release tags on the redesign branch can publish. Added aRun test suitestep to the release job so a tag cannot be published from a broken commit. Added an optionalPublish to npm registrystep that runs only when theNPM_TOKENsecret is configured; otherwise it prints an explicit "skipped" message so users running forks see what would happen. The release body now documents the fullopencode-rpc update --ref/--dev/--repoinstall flow instead of just the npm install command.
opencode-rpc update --ref REF: install a specific git ref (branch, tag, or commit SHA). Use this for pre-release branches likeredesign/v3-daemoninstead ofnpm install -g <url>#<branch>, which hits a npm v11 bug that creates a partiallib/node_modules/opencode-rich-presence/directory (onlysrc/, nopackage.json, nobin/) and never creates theopencode-rpcsymlink. The channel label written to the.install-channelmarker is inferred: refs matching a semver pattern arestable, anything else isdev.--refis mutually exclusive with--stableand--dev; passing any combination errors out (POSIX Guideline 11).opencode-rpc update --dev [BRANCH]: install latest commit on BRANCH. BRANCH is optional; if omitted, defaults tomain. Important:mainis currently v2.1.1 (pre-redesign). Users on v3 must pass the branch explicitly, e.g.--dev redesign/v3-daemon, or they will be downgraded to v2.x.opencode-rpc update --repo OWNER/REPO: install from a fork instead of the upstreamKhip01/opencode-rich-presence. Use this to test changes in your own fork before opening a PR. Combine with--dev,--stable, or--ref. Validated against^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$.runNpmInstallno longer usesgit fetch --depth=1 origin <ref>. That pattern failed for commit SHAs because git treats SHAs as refs in fetch but only finds them when they're in the local shallow history, which--depth=1does not provide. Replaced with a fullgit clonefollowed by directgit checkout <ref>, which handles branch names, tag names, AND commit SHAs (full or short) uniformly.tests/directory with three harnesses for regression:tests/phase1-harness.mjs: 46 scenarios covering event capture, state transitions, template renders, multi-instance state files.tests/phase2-harness.mjs: 21 scenarios covering daemon spawn-on-firing, state forwarding, multi-instance share, exit lifecycle.tests/phase2-v2-harness.mjs: 10 scenarios for the new behaviors (final-state push, fingerprint skip, extended grace, exit cancellation).
src/worker/daemon.mjs:fingerprintRendered()helper andlastPushedFingerprinttracking. Pushes whose payload matches what is already on Discord are skipped (no spam).src/worker/daemon.mjs:finalStateTimerandscheduleFinalStatePush()for the delayed push on rapid state changes.
src/worker/daemon.mjs. Long-lived Node.js subprocess that holds the Discord IPC connection. Listens on~/.config/opencode/.opencode-rich-presence.sockfor plugin clients. Picks the global most-recently-active session across all connected instances and pushes its rendered payload.src/worker/discord-ipc.mjs. Minimal inline Discord IPC client (replaces@xhayper/discord-rpc). Direct Unix socket, 30-second configurable timeout, fire-and-forget SET_ACTIVITY, no internal retry chain.src/plugin/daemon-client.js. Plugin-side socket client. Sends hello / state / goodbye to the daemon. Reconnects automatically if the daemon restarts.src/plugin/daemon-spawner.js. Spawns the daemon on first firing (chat.message). Polls for the daemon socket file to appear, then returns success so the plugin can connect.- Local Unix socket IPC: Newline-delimited JSON over
~/.config/opencode/.opencode-rich-presence.sock. - Daemon PID file at
~/.config/opencode/.opencode-rich-presence.pidso the plugin and CLI can check if the daemon is running. - First-firing trigger: the daemon spawns when the FIRST OpenCode instance fires a chat.message (not on plugin load). This avoids starting Discord connections for sessions that never need them. The user picked this trigger specifically.
- Daemon lifecycle: spawns on first fire, exits after
EXIT_GRACE_MS(2s) when the last instance sends goodbye. Survives Discord IPC drop (reconnects with 5s to 30s backoff). - Push throttling:
DISCORD_PUSH_INTERVAL_MS(4s) between SET_ACTIVITY calls. Resets when the picked instance changes (legitimate switch) so the user does not see a 4s lag when they switch terminals. opencode-rpc restartnow kills the running daemon (SIGTERM- 2s grace + SIGKILL) and rotates the activity log.
opencode-rpc uninstallnow signals the daemon to stop and removes the socket + PID file.opencode-rpc infoshows daemon socket presence, PID, alive status.
- Plugin (
src/plugin/local-presence.js) renders the presence payload locally then sends the RENDERED payload to the daemon. Daemon is dumb: it picks one and pushes. JSON serialization over the local socket loses class methods/getters on the SessionState, so we render in the plugin and ship the result. - Plugin (
src/plugin/index.js) callsensureDaemonAndConnect()on chat.message to spawn the daemon on first firing. Subsequent fires reuse the existing connection. - Plugin dispose sends a goodbye message to the daemon so the daemon can decrement its instance count promptly.
- Phase 1 harness: 47/47 scenarios pass.
- Phase 2 harness: 19/19 scenarios pass (daemon spawn, state forwarding, multi-instance share, lifecycle).
- Phase 2 E2E: plugin sends state, daemon picks session, daemon pushes to Discord (verified via daemon push log entries).
- 26 files syntax-check pass, 32 files smoke-test pass.
Phase 1 of the v3 redesign. No Discord push in this release. The plugin only collects local state, renders presence payloads, and writes a comprehensive chronological activity log so the user can verify the plugin's behavior end-to-end before Phase 2 wires the actual Discord push via a daemon subprocess.
This is a deliberate step back from the v2.x per-session worker architecture. The user reported that multi-terminal handoff ("display disappears then reappears") could not be fixed without removing the cause (per-session worker = per-handoff reconnect). Phase 1 establishes the local-first diagnostic surface; Phase 2 adds the daemon that eliminates the reconnect on handoff.
- Discord push. No subprocess worker, no Discord IPC, no
@xhayper/discord-rpcdependency. The plugin still renders the presence payload but logs it instead of pushing it. src/worker/discord-worker.mjs. Replaced by Phase 2's daemon.src/plugin/discord-service.js. Replaced bysrc/plugin/local-presence.js(render + log only).src/plugin/coordinator.js. No leader election in Phase 1; each OpenCode instance is independent.src/plugin/worker-spawner.js. No worker subprocess to spawn.@xhayper/discord-rpcdependency (no longer needed).prepareConnect,shutdownWorker,destroy,startConnect,getStatusLOCK_FILEandHANDOFF_REQUESTruntime files (the leader election infrastructure that backed them is gone).
- Activity log at
~/.config/opencode/presence-activity.log. Append-only chronological log of every plugin action. Each entry:[ISO timestamp] [pid N] [tag] message. PID tagging lets the usergrep "\[pid 12345\]"to follow one OpenCode instance when multiple are open. src/plugin/local-presence.js. Renders the presence payload (template + variables) and logs the result as awould-pushentry. Phase 2 will replace the log call with a daemon send call; the render logic stays unchanged.- Per-instance state files at
~/.config/opencode/presence-state-pid<pid>.txt. Each OpenCode instance writes its own file with its own snapshot, so multi-instance runs do not race on a single shared file. opencode-rpc infonow tails the last 30 activity-log entries inline and lists per-instance state files. This makes the activity log the de facto diagnostic surface.opencode-rpc restartnow rotates the activity log (renames to.prev) so the user can start fresh. Phase 2 will redefine it to manage the daemon subprocess.opencode-rpc uninstallnow removes the activity log, all per-instance state files, and the legacy lock file.
- All event handlers in
src/plugin/index.js(chat.message,session.created/updated/deleted/status/idle,message.updated/part.updated,permission.asked/replied) now log every event with key fields and only log state transitions on actual state changes (no spam on every event). src/shared/logger.jsadds anactivity(tag, message)helper for structured log entries. Existinglog()continues to write to the debug log for backward compatibility.src/cli/install.jsno longer adds@xhayper/discord-rpcto~/.config/opencode/package.json. There are no runtime dependencies in Phase 1.src/cli/uninstall.jsno longer prunes@xhayperfrom~/.config/opencode/node_modules/. Phase 1 has no runtime deps to clean up.src/cli/restart.jsno longer kills a worker subprocess (there is no worker). It now rotates the activity log.
AGENTS.mdrewritten for Phase 1. Documents the activity log format, the per-instance state file naming, the rationale for removing the per-session worker, and the lessons learned from the v2.0.x -> v3 redesign effort.docs/ARCHITECTURE.mdrewritten (no-subprocess design + Phase 2 daemon preview).docs/TROUBLESHOOTING.mdadds Phase 1 activity-log guidance.docs/INSTALL.mdadds v3 install command + Phase 1 differences.docs/CLI-REFERENCE.mdupdated for restart/info/install/uninstall.README.mdupdated to v3 Phase 1 status.
Phase 1 of the v3 redesign. No Discord push in this release. The plugin only collects local state, renders presence payloads, and writes a comprehensive chronological activity log so the user can verify the plugin's behavior end-to-end before Phase 2 wires the actual Discord push via a daemon subprocess.
This is a deliberate step back from the v2.x per-session worker architecture. The user reported that multi-terminal handoff ("display disappears then reappears") could not be fixed without removing the cause (per-session worker = per-handoff reconnect). Phase 1 establishes the local-first diagnostic surface; Phase 2 adds the daemon that eliminates the reconnect on handoff.
- Discord push. No subprocess worker, no Discord IPC, no
@xhayper/discord-rpcdependency. The plugin still renders the presence payload but logs it instead of pushing it. src/worker/discord-worker.mjs. Replaced by Phase 2's daemon.src/plugin/discord-service.js. Replaced bysrc/plugin/local-presence.js(render + log only).src/plugin/coordinator.js. No leader election in Phase 1; each OpenCode instance is independent.src/plugin/worker-spawner.js. No worker subprocess to spawn.- v2.x
prepareConnect(),shutdownWorker(),destroy(),startConnect(),getStatus()exported fromdiscord-service.js. None of these are needed in Phase 1. LOCK_FILEandHANDOFF_REQUESTruntime files (the leader election infrastructure that backed them is gone).
- Activity log at
~/.config/opencode/presence-activity.log. Append-only chronological log of every plugin action. Each entry:[ISO timestamp] [pid N] [tag] message. Tags includeload,config,models,restore,event,state,session,stats,queue,display,template,check,push,presence. PID tagging lets the usergrep "\[pid 12345\]"to follow one OpenCode instance when multiple are open. src/plugin/local-presence.js. Renders the presence payload (template + variables) and logs the result as awould-pushentry. Phase 2 will replace the log call with a daemon send call; the render logic stays unchanged.- Per-instance state files at
~/.config/opencode/presence-state-pid<pid>.txt. Each OpenCode instance writes its own file with its own snapshot, so multi-instance runs do not race on a single shared file. opencode-rpc infonow tails the last 30 activity-log entries inline and lists per-instance state files. This makes the activity log the de facto diagnostic surface.opencode-rpc restartnow rotates the activity log (renames to.prev) so the user can start fresh. Phase 2 will redefine it to manage the daemon subprocess.opencode-rpc uninstallnow removes the activity log, all per-instance state files, and the legacy lock file.
- All event handlers in
src/plugin/index.js(chat.message,session.created/updated/deleted/status/idle,message.updated/part.updated,permission.asked/replied) now log every event with key fields and only log state transitions on actual state changes (no spam on every event). src/shared/logger.jsadds anactivity(tag, message)helper for structured log entries. Existinglog()continues to write to the debug log for backward compatibility.src/cli/install.jsno longer adds@xhayper/discord-rpcto~/.config/opencode/package.json. There are no runtime dependencies in Phase 1.src/cli/uninstall.jsno longer prunes@xhayperfrom~/.config/opencode/node_modules/. Phase 1 has no runtime deps to clean up.src/cli/restart.jsno longer kills a worker subprocess (there is no worker). It now rotates the activity log.
AGENTS.mdrewritten for Phase 1. Documents the activity log format, the per-instance state file naming, the rationale for removing the per-session worker, and the lessons learned from the v2.0.x -> v3 redesign effort.docs/ARCHITECTURE.mdrewritten for Phase 1. Documents the no-subprocess design, the multi-instance behavior, the activity log format, and a Phase 2 preview of the daemon architecture.docs/TROUBLESHOOTING.mdadds a Phase 1 section on how to read the activity log, plus Phase-1-specific root causes (events seen but no state transition, activity log empty even though OpenCode is running, etc.).README.mdupdated to reflect the Phase 1 status (no Discord push yet) and the install command for the redesign branch.
Patch release. v2.1.0 was tagged with a broken src/cli/update.js (see "Fixed" below), so this release also ships the originally-intended fix plus the four features that landed on main between the v2.1.0 tag and this commit: the --stable flag, the .install-channel marker written by update, the channel suffix in version output, and the marker bootstrap in bin/opencode-rpc.js.
opencode-rpc update --stableflag. Skips version comparison and always installs the latest stable release tag. Use this to switch back to the stable channel when you have been running onupdate --devmode and want to pin to a tagged release without manually looking up the tag yourself.--stableand--devare mutually exclusive: passing both exits with code 2 and a clear error message, following POSIX Guideline 11 and modern CLI conventions (cargo, kubectl, npm).opencode-rpc update(all paths: default,--stable,--dev) writes a.install-channelmarker file inside the installed package recording the install channel (stablefor tag installs,devfor SHA installs), the ref used, and the install timestamp.opencode-rpc versionreads the marker and appends the channel info to its output: e.g.opencode-rich-presence v2.1.1 (stable)oropencode-rich-presence v2.1.1 (dev: 0f39f8a). Pre-v2.0.9 installs without a marker still show just the version. The marker lives inside the package directory (resolved vianpm root -grather thanimport.meta.urlbecause the package is replaced during install andimport.meta.urlkeeps the pre-install path).bin/opencode-rpc.jsbootstraps a.install-channelmarker on every CLI invocation if one doesn't already exist (defaults tochannel: "stable"). This ensuresopencode-rpc versionshows channel info even for users who installed vianpm install -g <tarball>directly without going throughopencode-rpc update, and for pre-v2.0.9 installs that were upgraded in place..gitignoreexcludes.install-channelas a safety net in case the marker is ever written into the repo working directory.
- v2.1.0's
src/cli/update.jswas tagged with the broken implementation that callednpm install -g <repo>#<ref>directly. The clone+pack+install-from-tarball fix was committed AFTER the v2.1.0 tag was cut, so users who installed v2.1.0 and then ranopencode-rpc update --dev(or any other update path) hitENOTDIR: not a directory, rename ...on npm v11. v2.1.1'supdate.jscontains the intended fix for all paths: clone the repo, check out the requested ref, runnpm pack, andnpm install -gthe resulting local tarball. Tarballs are not affected by the npm-v11 git-dep symlink bug. Also cleans up any leftover broken symlink atlib/node_modules/opencode-rich-presencebefore the install, so users with a broken state from v2.0.x or v2.1.0 recover automatically.
src/cli/update.js cleanExistingInstall()now resolves the install path vianpm root -g(authoritative, works for nvm/asdf/volta/custom prefixes) instead of guessing fromNVM_BIN/process.execPath/npm_config_prefix. Also usesrmSync(..., { recursive: true, force: true })which handles real directories, broken symlinks, and stale files uniformly (previously only unlinked the target, which silently failed for real directories).src/cli/help.jsUpdate section now includes theopencode-rpc update --stableexample alongside the existing--devexample; install example uses v2.1.1.
docs/INSTALL.mdadds a zsh quoting callout. zsh interprets#as a glob qualifier, sonpm install -g Khip01/opencode-rich-presence#v2.1.1errors with "no matches found". The URL must be single-quoted:npm install -g 'Khip01/opencode-rich-presence#v2.1.1'. bash and fish users are unaffected.docs/TROUBLESHOOTING.mdadds a new section "opencode-rpc: command not found after install" documenting how to recover from a broken-symlink state left by v2.1.0 (or by manualnpm install -g <repo>without a ref).
- Per-leader health check (self-heal watchdog): if the worker's last reported event is older than
STALE_WORKER_THRESHOLD_MS(15s) and the worker is not connected, the plugin kills and respawns it. Catches the "stuck worker" failure mode from any cause (reboot, Discord restart, IPC in an unexpected state) without forcing a fresh worker on every handoff.
- CRITICAL: Discord presence stuck after last OpenCode
/exitwas a silent sendCmd bug, not just an orphan-worker bug.discord-service.js shutdownWorker()/destroy()/forceRestartWorker()were callingsendCmd({ cmd: "shutdown" })AFTER nullingstate.workerProcess.sendCmdcheckedstate.workerProcess?.stdin?.writableand returned false silently because the ref had already been nulled. The shutdown command was therefore never written to the worker's stdin; the worker never ranclearActivity; the parent's SIGKILL at 2.5s killed the worker abruptly with the activity still set in Discord. The "stuck display" the user reported after closing all OpenCode instances was Discord keeping the last SET_ACTIVITY it received (because no CLEAR_ACTIVITY ever reached it). Fix:sendCmd(cmd, wp)now accepts the worker process as an explicit parameter; all three shutdown paths now pass the localwpvariable (captured before nulling state), and they nullstate.workerProcessAFTER sending the command instead of before.sendCmdalso now logs when it fails to write, so silent drops are no longer invisible in the debug log. - Discord presence "stuck" after the LAST OpenCode instance exits with
/exit: in some cases the worker'sclearActivityandclient.destroyboth hang past their timeouts. v2.0.8-rc5 deliberately dropped the parent's SIGKILL-after-grace fallback (theoretical PID-reuse concern), so the worker became an orphan that never exited. While alive, the orphan kept the IPC socket bound and Discord kept displaying the last activity. v2.1.2 fixes this with three layered defenses:- Parent SIGKILL fallback:
discord-service.js destroy()andshutdownWorker()nowwp.kill("SIGKILL")after a 2.5s grace if the worker is still alive. v2.0.8-rc5's "leave it orphaned" stance is reverted. Safe in practice: the polling loop checkswp.exitCodeevery 50ms and returns early if the worker exits, so the SIGKILL only fires when we know the worker is still alive (PID has not been recycled). - Worker self-exit kill switch:
discord-worker.mjsshutdown handler installs a 2ssetTimeout(... process.exit(0))that force-exits if the cleanup steps hang. Self-targeted exit has no PID-reuse concern. gracefulExitSIGINT/SIGTERM kill switch bumped 150ms → 1500ms: gives the in-flightclearActivity(1s timeout) enough headroom to land before force-exit. These three combine so the worker reliably exits within ~2-3s of shutdown even when Discord is unresponsive, releasing the IPC socket and clearing the activity.
- Parent SIGKILL fallback:
- Worker shutdown handler's
clearActivityretried once with a 500ms timeout before giving up. Combined with the parent's SIGKILL fallback, the worker now reliably exits within ~2-3s of receiving the shutdown command even if the Discord side is unresponsive.
- Pending:
docs/TROUBLESHOOTING.mdwill get a new section "Leader handoff does not update Discord display" once this fix is verified end-to-end.
- Replaced
@xhayper/discord-rpcwith a minimal inline Discord IPC client (src/worker/discord-ipc.mjs). The library had a hardcoded 10-second IPC handshake timeout inClient.connect()(setTimeout(..., 10e3)) that fired before the handshake READY frame was processed, even when Discord's IPC socket responded in milliseconds. We saw this in user testing: Discord IPC responds in <10ms but@xhayperrejects at exactly 10006ms with "Connection timed out", causing the worker to enter a slow retry loop with no progress. All major OpenCode Discord plugins (Puri12, phoenixak, butterbrodskiy) use the same library and have the same issue. The replacement client supports configurable timeout (default 30s), uses a direct Unix socket connection, and is fire-and-forget for SET_ACTIVITY (we do not parse RPC responses, just push presence). package.jsondependencies now empty. All Discord RPC communication is via the new inline client.opencode-rpc installandopencode-rpc uninstallupdated to prune legacy@xhayper/discord-rpcfrom~/.config/opencode/package.jsonif present.
2.1.0 - 2026-07-04
- Install workflow changed: end users now install via
npm install -g Khip01/opencode-rich-presence#v2.1.0instead of downloading a tarball from a GitHub Release URL. The repo at a specific tag is the install source, so there is no separate artifact to keep in sync with the source. The GitHub Releases workflow is preserved but its job-levelifcondition skips any tag containing-rc/-beta/-alpha; only stable tags create a GitHub Release (which now points users to the git install command and provides the tarball as a fallback for offline installs). - There is no longer a separate "pre-release" channel (
-rc,-beta,-alphatags). Useopencode-rpc update --devto test a fix before tagging stable.
opencode-rpc updatenow runsnpm install -g Khip01/opencode-rich-presence#<latest-stable-tag>instead of downloading a tarball from the GitHub Release API. The internal download / extract / install pipeline is gone.- Added
opencode-rpc update --devfor developers: fetches the latest commit SHA onmainand runsnpm install -g Khip01/opencode-rich-presence#<sha>. No more waiting for a release tag to test a fix. - Removed the
--prerelease/--preflag fromopencode-rpc update(no more pre-release channel exists).
- All install instructions across README.md, docs/INSTALL.md, docs/CLI-REFERENCE.md, docs/TROUBLESHOOTING.md, and CHANGELOG.md updated to use the git-based install command.
- docs/CLI-REFERENCE.md
opencode-rpc updatesection rewritten with three examples (up-to-date, stable update, dev update).
2.0.9 - 2026-07-04
opencode-rpc update(without--prerelease) reported "Already up-to-date" when the user was on a prerelease build (e.g.v2.0.8-rc5) and the latest GitHub release was the matching stable (v2.0.8). Root cause: the previous version-comparison logic skipped any update where the numeric versions matched, without considering prerelease-to-stable transitions as a normal upgrade path. v2.0.9 replaces the binary "sameBase -> skip" with explicit decision rules:cmp > 0(strict numeric upgrade) -> update.cmp < 0(current is newer) -> skip.cmp === 0, current is prerelease, latest is stable -> update (prerelease -> stable is a normal upgrade; does not require--prerelease).cmp === 0, current is stable, latest is prerelease -> only update with--prerelease(so a user on stable v2.0.8 is not auto-bumped to v2.0.8-rc9).cmp === 0, both are prereleases,--prereleaseset -> update only if the tag suffix differs (e.g. rc4 -> rc5).- Otherwise -> up-to-date.
2.0.8 - 2026-07-03
Stable release. Cumulative fixes since v2.0.7 across five pre-release candidates (rc1 through rc5), all of which the user confirmed working in multi-instance testing:
discord-service.js:shutdownWorker()no longer signal-kills an already-exited worker (PID-reuse race that was killing the new leader's worker withcode=null sig=SIGKILL/SIGTERMright after a handoff).- Multi-instance leader oscillation is dampened with
LEADER_COOLDOWN_MSand by restricting handoff requests to user-initiated events only (chat.message,permission.asked,permission.replied). Agent-side events stillmarkActivebut do not request leadership. - Multi-instance handoff latency reduced from ~5-8 seconds to ~250ms-1s by removing the 2-second fixed IPC delay, dropping
LEADER_COOLDOWN_MSto 3s, reducingHEARTBEAT_INTERVALto 2s, addingACTIVE_HANDSHAKE_INTERVAL(250ms fast-poll) for standbys that recently marked themselves active, and reducing Discord worker retry backoff (initial 500ms, cap 5000ms). discord-service.js:destroy()now uses the same poll-for-exit pattern asshutdownWorker(), fixingWorker exited: code=null sig=SIGKILLduring plugin dispose.- New leader now waits 2 seconds before connecting (REMOVED in rc5; the SIGTERM-after-exit fix made it unnecessary), and forces
checkAllSessionsActivity()after gaining leadership to refresh stale in-memory session states. - Discord presence was "stuck" after OpenCode exited. Worker shutdown now calls
clearActivity()before destroying the Discord client, andSIGINT/SIGTERMsignal handlers do the same. - New leader pre-spawns its worker via
prepareConnect()on user-initiated events when standby, eliminating the "display torn down and rebuilt" feeling during terminal switching. opencode-rpc update --prereleasewas reporting "Already up-to-date" when upgrading stable v2.0.8 to a prerelease on the same base version, becauseparseSemverstripped the prerelease suffix. Now compares both numeric and tag.
- Worker log messages are now forwarded into the parent's debug log (
[worker] ...lines). opencode-rpc update --prerelease(alias--pre) opts in to GitHub releases marked as prerelease. Tags containing-rc,-beta, or-alphaare marked prerelease in.github/workflows/release.ymlso stableopencode-rpc updatedoes not pick them up. Use this flag to test pre-release builds before they are promoted to stable.
2.0.8-rc5 - 2026-07-03
opencode-rpc update --prereleasewas reporting "Already up-to-date" when the user's installed package wasv2.0.8(stable) and the latest GitHub release wasv2.0.8-rc4(prerelease) becauseparseSemverstrips the-rc4suffix and the numeric comparison returned0(equal). v2.0.8-rc5 compares both the numeric version AND the tag, so a stable-to-prerelease upgrade on the same base version is now detected and offered.- Discord worker was hanging for >2s during shutdown. The
shutdowncommand handler calledawait clearActivity()followed byawait client.destroy(), both of which could hang indefinitely if Discord's IPC socket was in a bad state. Without timeouts, the parent'sdestroy()/shutdownWorker()polled for 2s and then sentSIGKILL. v2.0.8-rc5 bounds both calls with a 1s timeout each, so the worker always exits within ~2.2s of receiving the shutdown command. - Removed the
SIGKILL-after-grace fallback fromdiscord-service.js:destroy()andshutdownWorker(). Even with the polling pattern added in v2.0.8-rc3, there is a window where the worker process has exited at the OS level but Node.js has not yet dispatched theexitevent to the parent.wp.exitCodeis stillnullduring this window, the parent would still sendSIGKILL, and Linux could have already recycled the PID to the next leader's worker. The user observed exactly this symptom: the new leader's worker dying withcode=null sig=SIGKILL(orSIGTERM) right after a handoff. v2.0.8-rc5 leaves the worker to exit naturally on its own; if it does hang, it becomes an orphan that gets cleaned up when its parent eventually exits.
2.0.8-rc4 - 2026-07-03
- Multi-instance handoff no longer restarts the Discord presence. Previously, every time the standby instance took over leadership, it would kill the old worker's Discord IPC connection (Discord sees this as the user going offline), spawn a new worker, and reconnect from scratch. Discord would briefly show no presence, then re-show it after the new login completed (1-3 seconds). From the user's perspective this looked like the display was being torn down and rebuilt on every terminal switch.
- v2.0.8-rc4 adds
prepareConnect()indiscord-service.js. When a standby instance receives a user-initiated event (chat.message,permission.asked,permission.replied), it pre-spawns its worker BEFORE becoming leader. The worker immediately tries to log in to Discord, fails (the IPC socket is still held by the current leader), and retries with the fast backoff (initial 500ms, cap 5s) configured indiscord-worker.mjs. When the standby eventually becomes leader, its worker is already running and the next retry tick lands on a free IPC socket, so the only remaining delay is the Discord IPC handshake itself.
2.0.8-rc3 - 2026-07-03
- Discord presence was "stuck" after OpenCode exited. The worker's shutdown handler called
client.destroy()without first sending aclearActivityIPC frame, so Discord never received a clear-presence command and kept showing the last activity. The user had to quit and reopen Discord to clear it. v2.0.8-rc3 makes the worker'sshutdowncommand handler and theSIGINT/SIGTERMsignal handlers callclearActivity()first, thenclient.destroy(). AshuttingDownflag suppresses the resultingdisconnectedevent's reconnect attempt so the worker exits cleanly instead of looping on a reconnect timer. discord-service.js:destroy()had the same SIGTERM-after-exit race asshutdownWorker()did pre-rc1: it sentSIGTERM200ms after the shutdown command regardless of whether the worker had exited. Replaced with the same poll-for-exit pattern used byshutdownWorker(pollchild.exitCode/child.signalCodefor up to 2s, only force-kill if still alive). This was the source ofWorker exited: code=null sig=SIGKILLlogs appearing during plugin dispose.
- Worker log messages are now forwarded into the parent's debug log. The parent plugin's debug file now shows
[worker] ...lines for "Worker started", "Activity sent", "Replay activity failed", "Shutdown requested", "Discord READY event", "Discord disconnected", etc. This makes it possible to diagnose connection problems without enabling extra debug flags.
2.0.8-rc2 - 2026-07-03
- Handoff latency reduced from ~5-8s to ~250ms-1s. v2.0.8-rc1 had a 2-second fixed delay before the new leader connected to Discord, plus an 8-second
LEADER_COOLDOWN_MSwindow where the leader ignored handoff signals, plus a 5-second leader heartbeat that bounded how often the handoff check ran. v2.0.8-rc2 removes the fixed delay (the previous leader's worker exits in milliseconds via the shutdown command, so no extra wait is needed), dropsLEADER_COOLDOWN_MSto 3 seconds (just enough to debounce rapid chat bursts from the same user), and reducesHEARTBEAT_INTERVALto 2 seconds so the handoff check fires twice as often. - Standby polling switches to a fast 250ms interval (
ACTIVE_HANDSHAKE_INTERVAL) for 8 seconds after the standby marks itself active. An actively-requesting standby now acquires the lock within a fraction of a second of the leader releasing, instead of waiting up to 2 seconds on the slowHANDOFF_CHECK_INTERVALpoll. - Discord worker retry backoff is now much faster. Initial retry dropped from 3000ms to 500ms, and the cap dropped from 30000ms to 5000ms. The previous values made the new leader's worker sit idle for 3+ seconds after a Discord IPC handshake failed before retrying, which compounded the visible presence gap during handoff.
2.0.8-rc1 - 2026-07-03
discord-service.js:shutdownWorker()no longer SIGTERMs (or SIGKILLs) the old worker after it has already exited. The previous implementation sentkill("SIGTERM")200ms after the shutdown command regardless of whether the worker had exited. Because Linux reuses PIDs, the new leader's worker could spawn with the same PID as the old one and then receive a stray SIGTERM from the old leader's cached ChildProcess reference, killing it withcode=null sig=SIGTERM. The new implementation pollschild.exitCodeandchild.signalCodefor up to 2s and only force-kills if the worker is genuinely still alive. Removes the "display closes when leader changes" symptom.- Multi-instance leader oscillation is dampened with a leader cooldown. Previously, every instance saw every SDK event (
message.part.updated,session.status, etc.), so any active instance would write a handoff signal every event, and the leader would yield as soon as its standby'slastActivitywas fresher. With multiple active windows, leadership ping-ponged back and forth, causing visible Discord presence flicker every 5 seconds. The leader now ignores handoff signals forLEADER_COOLDOWN_MS(8 seconds) after becoming leader, so the active window keeps Discord presence for at least that long. chat.message,session.created/updated,session.status,message.updated, andmessage.part.updatednow opt out of the handoff request by default (noteActivity({ requestHandoff: false })). Onlychat.message,permission.asked, andpermission.repliedrequest handoff, because those are the events that indicate the user is actively interacting with this instance. Agent-side events still mark the instance active but do not request leadership, which further reduces oscillation.- The new leader now waits 2 seconds before connecting to Discord, so the previous leader's worker has time to fully release the Discord IPC socket. Without this delay, the new worker could race against the still-cleaning-up old connection and fail its first login (it retries with backoff, but the user sees a presence gap).
- The new leader now forces
checkAllSessionsActivity()after gaining leadership, so the in-memory session states are refreshed from the server. Previously, standby instances did not poll for activity (only the leader did), so a freshly-promoted standby could be showing a staleTypingstate even though the model had already finished.
opencode-rpc update --prerelease(alias--pre) opts in to GitHub releases marked as prerelease. Tags containing-rc,-beta, or-alphaare now marked prerelease in.github/workflows/release.ymlso stableopencode-rpc updatedoes not pick them up. Use this flag to test pre-release builds before they are promoted to stable.
2.0.7 - 2026-07-01
- Multi-instance leader election now uses activity-based handoff. Previously, the first OpenCode instance to start held the leader lock until exit or 15s of staleness. Standby instances never pushed to Discord, even when actively chatting. Result: a previously idle leader kept showing stale presence while another instance was actively generating messages.
- When a standby instance receives a
chat.message(or any other activity-implying event), it writes a handoff signal at~/.config/opencode/.opencode-rich-presence-handoff. The current leader's heartbeat loop reads the signal on each tick and releases the lock if it sees a fresher request from a different PID. The standby then acquires the lock on its next 2s poll and starts pushing to Discord. - A leadership-change callback in
index.jscallsstartConnect()on gain andshutdownWorker()on loss so the new leader's Discord worker actually starts and the old leader's worker actually stops (without permanently disposing the service, so the instance can re-acquire leadership later). - Added
discord-service.js:shutdownWorker()for temporary teardown on leadership loss. Distinct fromdestroy()which is permanent and used only on plugin dispose. The new function sets anintentionalShutdownflag that the workeronExithandler reads to skip its respawn/retry logic. - Added
src/plugin/coordinator.js:requestHandoff()for standby instances to signal they want leadership, andmarkActive()to record local activity timestamps. Standby instances also poll every 2s for lock release or staleness so they take over automatically when the leader yields or crashes. - Lock file format now includes
lastActivity(timestamp). The leader's heartbeat writes its currentlastActivityon each tick; the heartbeat uses this to decide whether a handoff request is fresher than its own activity.
- The plugin's event handlers (
chat.message,message.part.updated,permission.asked,permission.replied,session.statuswhen busy, etc.) now call anoteActivity()helper that updates the locallastActivitytimestamp and triggers a handoff request if the instance is a standby. Previously these events updated only the per-session state.
opencode-rpc installno longer addsopencode-rich-presenceto thepluginarray in~/.config/opencode/opencode.jsonc(or.json). The v2.0.5 install path wrote that entry, but OpenCode reads the array as a list of npm packages to fetch on startup. The package is not published to npm (it is distributed via GitHub Releases tarballs only), so OpenCode returned a 404 notification on every launch. v2.0.6+ relies entirely on the symlink at~/.config/opencode/plugins/opencode-rich-presence.jsfor loading, which OpenCode does natively and which never triggers an npm fetch.opencode-rpc installnow detects and offers to remove a staleopencode-rich-presenceentry inopencode.jsoncleft over from a v2.0.5-era install. Default Yes. This silences the 404 notification on the next OpenCode restart.opencode-rpc uninstallnow auto-removes any staleopencode-rich-presenceentry inopencode.jsoncas part of cleanup, so users uninstalling the plugin do not carry a noisy 404 notification afterwards.opencode-rpc infono longer readsopencode.jsoncto check plugin registration. It now reports the symlink status of~/.config/opencode/plugins/opencode-rich-presence.js(path, whether it is a symlink, target). The section was renamed fromOpenCode plugin registrationtoOpenCode plugin symlinkand is now always shown.- Updated
docs/INSTALL.md,docs/CLI-REFERENCE.md,docs/ARCHITECTURE.md, anddocs/TROUBLESHOOTING.mdto remove all references to the now-defunctopencode.jsoncplugin registration step and to describe the symlink-only loading mechanism.
- The
OpenCode plugin registrationsection inopencode-rpc infooutput has been replaced withOpenCode plugin symlink. Always shown (no longer conditional onopencode.jsoncparsing).
2.0.5 - 2026-06-23
opencode-rpc installno longer hangs when the Discord config file already exists. The prompt helper now uses a single long-lived readline interface so prompts always receive input correctly.opencode-rpc installcan now auto-register the plugin in~/.config/opencode/opencode.jsonc(or.json). The user is asked for confirmation before any modification; if the file cannot be parsed, the installer falls back to clear manual instructions.opencode-rpc installnow symlinks the plugin entry into~/.config/opencode/plugins/opencode-rich-presence.jsand ensures@xhayper/discord-rpcis installed under~/.config/opencode/node_modules/. This works around the fact that the plugin is not on the npm registry: OpenCode can load it directly from disk instead of trying to fetch it via Bun and getting a 404.- The worker path in
src/shared/paths.jswas computed with the wrong number of../levels (resolving to<pkg-root>/worker/...instead of<pkg-root>/src/worker/...). The plugin would acquire the leader lock and stay "alive" but the worker subprocess it spawned failed instantly withMODULE_NOT_FOUND. Fixed to use../worker/discord-worker.mjs(one level up fromsrc/shared/). opencode-rpc uninstallnow also removes@xhayper/discord-rpcfrom~/.config/opencode/package.jsonand re-runsnpm installthere to prune the package fromnode_modules. Users no longer carry leftover presence plugin artifacts after uninstall.opencode-rpc installnext-steps message no longer suggests editing the config when a Discord App ID is already set. Users with a working configuration see only the restart instruction.opencode-rpc uninstallnow asks before deletingdiscord-config.json(default N), and backs up the file with a timestamp suffix when the user agrees. Previously the file was always left alone without explanation.opencode-rpc uninstallnow prints an explicit code snippet showing the"plugin"array entry to remove fromopencode.jsonc(with a<-- DELETE THIS LINEmarker). Previously the file was mentioned but no concrete example was given.opencode-rpc installnow manages~/.config/opencode/package.json(adds the dependency if missing) and runsnpm installin that directory. Existing entries are preserved.opencode-rpc uninstallremoves the local plugin symlink at~/.config/opencode/plugins/opencode-rich-presence.js(with confirmation).opencode-rpc uninstallno longer suggests the non-existent/configslash command. The Step 1 output now suggests using a text editor (nano) to editopencode.jsoncoropencode.jsondirectly.- Backup files are now explained as persistent in the home directory, not in
/tmp. The full backup path is printed so users can locate the backup. opencode-rpc uninstallnow detects and offers to remove a leftover~/.config/opencode/node_modules/directory from older install scripts. The check is gated on the@xhayperscope being present, and the script now also recognises OpenCode's own runtime cache (@opencode-aiscope) and leaves it alone with a clear explanation.- The JSONC parser used by both
opencode-rpc install(auto-register) andopencode-rpc info(plugin registration status) no longer treats://inside URLs as a line comment. The regex now uses a negative lookbehind on:so URLs like"https://example.com"and"http://127.0.0.1:8080"parse correctly. - The CLI process now exits cleanly after each command instead of hanging.
bin/opencode-rpc.jscallsprocess.exit(0)after a successful command so the readline interface (held by the prompt helper) does not keep the Node process alive afterDone.is printed.
- The prompt helper now supports multi-option prompts with a single long-lived readline interface, fixing both interactive hangs and piped input (e.g.,
echo "b" | opencode-rpc uninstall). - The installer's "next steps" message no longer tells users to wait for OpenCode's "auto-install via Bun" (which fails because the package is not on the npm registry). It now points users to the symlink in
~/.config/opencode/plugins/. docs/TROUBLESHOOTING.mdadds a "Known Root Causes" section with seven failure modes observed during v2.0.x development (worker path, missing npm registry, stale ESM cache, JSONC://bug, install/uninstall hangs, npm registry workaround). Also adds a quick diagnostic checklist at the bottom for triaging "Discord presence not showing" reports.
opencode-rpc restartno longer restarts Discord Desktop. It now only writes the restart signal and kills the worker subprocess (matches v1.0.0restart-discord.shbehavior). Discord Desktop is left alone, so users in voice chat are not disrupted.- CLI entry script now wraps execution in an async
main()function. Eliminates the Node.js warning "Detected unsettled top-level await" that appeared in Node 22+ when running interactive commands likeinstall,restart, anduninstall. opencode-rpc infocan now read~/.config/opencode/opencode.jsoncfiles that use JSONC features (line comments, block comments, trailing commas). Previously only strictly valid JSON was accepted, causing the "OpenCode plugin registration" section to be skipped on JSONC configs.- Documentation accuracy: replaced fabricated example outputs in
docs/CLI-REFERENCE.mdanddocs/TROUBLESHOOTING.mdwith actual outputs captured from the running CLI. Section visibility notes added so users know when "Lock (leader instance)" and "OpenCode plugin registration" sections appear or are absent. - Documentation updated across README, CLI-REFERENCE, TROUBLESHOOTING, and ARCHITECTURE to reflect the new restart behavior.
- Documentation polish: standardized punctuation and phrasing across README, CHANGELOG, and all docs files. No functional changes.
- CLI command renamed:
rich-presence->opencode-rpcfor clearer namespace ownership (this is an OpenCode ecosystem tool, not a generic rich presence tool). - Package name stays
opencode-rich-presence(unchanged). - Bin file renamed:
bin/rich-presence.js->bin/opencode-rpc.js. - CLI debug env var renamed:
RICH_PRESENCE_DEBUG->OPENCODE_RPC_DEBUG.
The npm package name is unchanged (opencode-rich-presence), so npm update -g opencode-rich-presence upgrades both v2.0.0 and v2.0.1 users. Only the CLI command name changed:
- v2.0.0:
rich-presence install - v2.0.1:
opencode-rpc install
2.0.0 - 2026-06-23
- Install via npm + GitHub Releases tarball instead of bash scripts.
- Cross-platform: Linux, macOS, Windows. Replaces Linux-only v1.0.0.
- Plugin code refactored into modular structure (
src/plugin/,src/shared/,src/cli/). - Config paths standardized to OpenCode's
~/.config/opencode/across all platforms (OpenCode normalizes this on Windows too). - CLI replaces bash scripts:
opencode-rpc install/uninstall/restart/update/info/help. - Plugin name renamed from
opencode-dc-too-rich-presencetoopencode-rich-presence. - Lock file renamed from
.opencode-dc-too-rich-presence.lockto.opencode-rich-presence.lock. - Debug log moved from hardcoded
/tmp/plugin-debug.logto OS temp directory viaos.tmpdir(). - Debug env var renamed from
OPENCODE_DC_TOO_RICH_DEBUGtoOPENCODE_RICH_PRESENCE_DEBUG.
- CLI tool with subcommands:
install,uninstall,restart,update,info,help,version. opencode-rpc updatefetches latest release from GitHub API and self-updates.- Cross-platform Discord restart logic (
pkill/osascript/taskkill). - Windows path detection in
findNodeExecutable(%ProgramFiles%,%LOCALAPPDATA%). - CI matrix testing on Linux, macOS, Windows across Node 18/20/22.
- Automated GitHub Release workflow (
.github/workflows/release.yml). docs/PLATFORM-NOTES.mdanddocs/CLI-REFERENCE.md.
- Bash install/uninstall/restart scripts.
- Hardcoded Linux paths.
- GNU sed/awk dependency.
v1.0.0 is preserved as opencode-rich-presence-v1.0.0-legacy-linux-only on the GitHub releases page. To migrate:
- Back up
~/.config/opencode/discord-config.json. - Install v2.0.0:
npm install -g Khip01/opencode-rich-presence#v2.0.0
- Run
opencode-rpc install. - Restore your settings into the new config (App ID, presence templates).
- Restart OpenCode.
1.0.0 - 2026-06-22
- Initial release.
- Linux-only Discord Rich Presence plugin for OpenCode.
- Bash install/uninstall/restart scripts.
- Template engine with variables, conditionals, fallbacks.
- Multi-instance coordinator (leader election via file lock).
- Discord subprocess worker (bypasses Bun IPC issues).
- 2-second IPC release delay on intentional restart.
- Configurable via
discord-config.json+ env vars. - Documentation: README, SETUP, ARCHITECTURE, CUSTOMIZATION, TROUBLESHOOTING.