feat(claude-code-plugin): OpenViking statusline (opt-in)#1890
feat(claude-code-plugin): OpenViking statusline (opt-in)#1890ZaynJarvis merged 16 commits intovolcengine:mainfrom
Conversation
A one-line OV status renders under the CC input box: server health, last-turn recall stats, pending capture, and queue alerts. Network calls share a 5 s file cache and have a 250 ms hard timeout so the statusline never blocks render. - scripts/statusline.mjs: main entry, ANSI degrade, 80-char cap - scripts/lib/state.mjs: atomic JSON state writer + TTL reader - scripts/lib/server-probe.mjs: cached /health (+ /observer/queue) - auto-recall / auto-capture: write last-recall.json / last-capture.json - setup-helper/install.sh: opt-in prompt; replace-or-skip for existing user statusline; backup + restore-instructions - bump plugin version 0.2.0 -> 0.3.0
Queue probe was sharing the /health 250 ms budget; on remote servers where /health used 200ms+, the queue probe got ~50ms or was skipped entirely, so queue_healthy flapped between false (when /health was fast) and null (when /health was slow). Result: ⚠ queue badge appeared intermittently even when the queue was consistently unhealthy. Worst-case statusline latency goes from 250ms to 500ms; typical case is unchanged (~150ms) since both endpoints respond in tens of ms when the server is healthy.
…e count - 250ms was too aggressive for remote OV servers; ordinary network jitter (200-400ms /health) was producing spurious "OV ✗ offline" flicker. Bumped to 1s per endpoint. Worst case render is now ~2s but the 5s cache amortises this to once per 5s window per session. - pending_tokens is a sawtooth: it climbs to commit_threshold then snaps to 0 on commit. Showing only "X/20k tok" of a long conversation read as "we only captured X tokens", which hid the work already archived. Now the statusline also shows "N arch" — the running commit_count pulled from the OV session metadata. So a long session now shows e.g. "✎ 573/20k tok · 2 arch" instead of just "✎ 573/20k tok".
QueueObserver.is_healthy() is derived from QueueManager.has_errors(), which is `any(q._error_count > 0 for q in queues)`. _error_count is a lifetime cumulative counter that never resets, so any server with a single transient embedding failure ever flips is_healthy to false forever — even when the next 1000 jobs all succeed. Real example from a production server: 41 jobs processed, 2 historic errors (95%+ success rate), is_healthy returns false. The badge then appears constantly for users whose OV experience is fine. Removing the badge and the second network round-trip. Connectivity (OV ✓), recall activity (↩ N mem), and capture progress (✎ N/20k arch) already cover whether OV is functioning end-to-end.
- ↩ N mem (0.92): max recall score appended in parens. Quality hint without an extra segment. auto-recall.mjs now writes top_score in last-recall.json. - ✗ N dropped: turns that auto-capture failed to push this batch. Not sticky — auto-capture overwrites last-capture.json each Stop hook, so transient failures clear themselves on next success. Sustained failures stay visible (which is when the user needs to know). - 🔗 resumed / 🔗 compact: session-start.mjs writes a 1-min TTL event when CC source is resume or compact. Lets the user see that OV did re-hydrate context across restarts instead of having to guess. - +N today: cross-session daily commit_count. auto-capture maintains daily-stats.json (resets on date rollover). Hidden when 0 to keep fresh-day mornings unobtrusive. Distinct from per-session "M arch" which only counts the current CC session. Truncation order verified: server → recall → capture → dropped (alert) → resumed (info) → today (info). 80-char cap drops the lowest-priority tail when the line gets crowded.
Recall side: `tokens_used` is a chars/4 heuristic (estimateTokens in auto-recall.mjs), not real tokens. For CJK-heavy text the heuristic underestimates by 2-4x, so labelling it "tok" is misleading. Capture side: `pending_tokens` comes from the server, but the server's own counter is also approximate. Mixing the two under the same label invites the wrong mental model. Just drop the unit. The magnitude is meaningful on its own (1.2k = medium injection, 573/20k = 3% of next archive). Configuration field names (recallTokenBudget, commitTokenThreshold) keep "Token" so we don't churn user-facing config.
…eading The "1.2k" between mem count and latency was estimateTokens(text) = ceil(text.length / 4) on the assembled injection block. For CJK-heavy content the heuristic underestimates by 2-4x, which is enough that showing the number does more harm than presenting count + score + latency alone. Capture side keeps "573/20k" because the server reports pending_tokens itself (more accurate, and the ratio against threshold is meaningful even if the absolute count is approximate).
…ompact Statusline expected `🔗 resumed/compact` to reflect that the event happened, but session-start.mjs only wrote the marker when `formatArchiveContext` had something to inject. Fresh sessions with no prior archive saw a `/compact` silently — statusline showed nothing, leaving the user wondering whether the hook fired at all. Move the writeJsonState call ahead of the no-archive early return and tag the payload with `had_context: false` for the empty case. The badge now fires on every resume/compact event with a 1-minute TTL. `✎` capture pending is unaffected — that segment is gated on `cc_session_id === sessionId` and after `/branch` there's no Stop hook for the new session yet, which is correct (stale capture from a different session would be misleading).
Statusline has more knobs than env vars expose — segment ordering, colors,
composing with another statusline, custom segments, state file shapes — and
the integration doc is the wrong venue for that level of detail. Add a
recipe-style guide aimed at an AI assistant reading it end-to-end, so users
can ask Claude Code "personalize my statusline" instead of spelunking source.
- examples/claude-code-memory-plugin/docs/STATUSLINE.md: recipes (drop a
segment, recolor, compose, reset state, add a custom segment) + state
file schemas + pointers to the canonical files. Defers env-var reference
back to docs/en/agent-integrations/02-claude-code.md.
- install.sh: print a copy-pasteable seed prompt at the end of install. Not
intrusive — no auto-launch, just a tip the user can ignore.
- docs/{en,zh}/agent-integrations/02-claude-code.md: cross-link the new doc
from the Statusline section.
Recipes referenced \`scripts/statusline.mjs\` etc. with no anchor, so an agent reading the doc had no way to resolve them — `~/.openviking/openviking-repo/examples/claude-code-memory-plugin/scripts/...` is far enough off the beaten path that "go look in scripts/" doesn't land. Define \`\$REPO\` / \`\$PLUGIN\` / \`\$STATE\` once at the top with how to verify each (jq on settings.json, find as fallback), then propagate the prefixes through every recipe. The install seed prompt already passes the absolute path of STATUSLINE.md, so the chain is now self-contained.
…pose install tip
Three small refinements after seeing the statusline in the wild:
- statusline.mjs: split the unhealthy branch — `OV ⚠ slow` (yellow) when
the probe times out, `OV ✗ offline` (red) when it errors. Slow ≠ dead;
red was alarmist for transient lag (e.g. remote SaaS GC pauses).
- examples/claude-code-memory-plugin/docs/STATUSLINE.md: add "What each
segment means" — a full glossary covering every state combination
(✓/⚠/✗/⚡, ↩, ✎ in its three forms, dropped, 🔗 resumed/compact, +N
today), plus a "missing when?" troubleshooting list. The integration
docs only had four example lines, two of which were stale; the canonical
reference now lives next to the code.
- docs/{en,zh}/agent-integrations/02-claude-code.md: refresh the example
block (drop stale `1.2k tok` / `12k/20k tok`, add ⚠ slow + 🔗 resumed
+ +N today rows), and broaden the cross-link to advertise both
explanation and personalization.
- install.sh: rewrite the seed prompt as "walk me through what each
segment means, then ask if I want to personalize" — covers the more
common "what does this badge mean?" path before customization.
VitePress only ships docs under \`docs/\`, but STATUSLINE.md lives in \`examples/claude-code-memory-plugin/docs/\` (next to the plugin code, where it logically belongs). Relative \`../../examples/...\` resolved on GitHub but 404'd on the published docs site. Use an absolute https://github.com/volcengine/OpenViking/blob/main/... URL — works in both renders, and a parenthetical note tells readers why.
It was meta — readers don't need to know why the link's absolute. Just click.
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
A docs/ folder with one file is awkward when README.md and README_CN.md already sit at the plugin root. Moves STATUSLINE.md alongside them and fixes up: - Stale opening line that pointed at the integration doc for the segment glossary — that glossary now lives in STATUSLINE.md itself, so the cross-reference is just for env vars. - Drop the "(path notation defined just below)" parenthetical (meta). - Update the install seed prompt and the en/zh integration cross-links to the new path.
PR Code Suggestions ✨No code suggestions found for the PR. |
There was a problem hiding this comment.
Pull request overview
Adds an opt-in Claude Code statusline for the OpenViking memory plugin, exposing server health plus recent recall/capture/session signals by composing local hook-written state with a short-lived /health probe cache.
Changes:
- Introduces
scripts/statusline.mjsto render a one-line, ANSI-colored (optional) statusline with truncation and fail-soft behavior. - Adds shared state + probe utilities (
lib/state.mjs,lib/server-probe.mjs) and updates hook scripts to write JSON snapshots used by the statusline. - Updates the installer to optionally register
.statusLinein~/.claude/settings.json, and refreshes docs/readmes to describe the new feature.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| examples/claude-code-memory-plugin/setup-helper/install.sh | Adds opt-in statusline registration and a post-install “seed prompt”. |
| examples/claude-code-memory-plugin/scripts/statusline.mjs | New statusline renderer reading hook state + cached health probe. |
| examples/claude-code-memory-plugin/scripts/session-start.mjs | Writes a short-lived session-event marker for the statusline. |
| examples/claude-code-memory-plugin/scripts/lib/state.mjs | Adds shared atomic-ish JSON state read/write helpers. |
| examples/claude-code-memory-plugin/scripts/lib/server-probe.mjs | Adds file-cached /health probe with timeout. |
| examples/claude-code-memory-plugin/scripts/auto-recall.mjs | Writes per-turn recall snapshot for statusline consumption. |
| examples/claude-code-memory-plugin/scripts/auto-capture.mjs | Writes per-turn capture snapshot + daily archive counter. |
| examples/claude-code-memory-plugin/README.md | Documents the new statusline feature. |
| examples/claude-code-memory-plugin/README_CN.md | Chinese documentation for the new statusline feature. |
| examples/claude-code-memory-plugin/docs/STATUSLINE.md | New personalization + segment glossary doc for assistants/users. |
| examples/claude-code-memory-plugin/.claude-plugin/plugin.json | Bumps plugin version to 0.3.0. |
| docs/en/agent-integrations/02-claude-code.md | Adds statusline documentation + link to STATUSLINE.md. |
| docs/zh/agent-integrations/02-claude-code.md | Adds Chinese statusline documentation + link to STATUSLINE.md. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Code: - state.mjs: derive STATE_DIR from \$OPENVIKING_HOME (with ~ expansion) so the override the docs already advertised actually works. Default unchanged. Was hard-coded to homedir(). - auto-recall.mjs: rename \`session_id\` → \`cc_session_id\` in last-recall.json to match last-capture.json / last-session-event.json. STATUSLINE.md schema already used \`cc_session_id\`. No reader filtered on the recall field, so this is a schema-cleanup, not a behavior change. - install.sh: quote the plugin path inside the JSON \`command\` value, so CC's /bin/sh -c invocation tolerates spaces / metacharacters in \$REPO_DIR (custom OPENVIKING_REPO_DIR locations). - install.sh: mktemp inside ~/.claude/ instead of \$TMPDIR, so the final rename is within one filesystem (atomic). Was crossing tmpfs/$HOME on Linux, where \`mv\` falls back to copy+unlink and isn't crash-safe. Comments / docs (drift from earlier "drop tok / 250→1000ms" passes): - server-probe.mjs: header comment said "Hard 250 ms" while the constant is 1000. Replaced with a forward-reference to the constant block which already explains the choice. - README.md / README_CN.md: refresh the example block (drop \`1.2k tok\` / \`12k/20k tok\`, add \`⚠ slow\` / \`🔗 resumed\` / \`+N today\` rows), correct the hard-timeout sentence (250 ms → 1 s), cross-link STATUSLINE.md. - install.sh: the \`info\` sample at registration time was also stale.
Statusline is additive and opt-in — no API breaks, no behavior change for existing installs that skip the prompt. A patch bump fits better than a minor.
Description
Adds an optional one-line OpenViking statusline rendered below the Claude Code input box. Surfaces live signals from
auto-recall/auto-capture/session-starthooks plus a 5 s shared cache ofGET /health, so users get visibility into what OV is doing without leaving the TUI. Opt-in at install time — existing user statuslines aren't clobbered.Related Issue
N/A.
Type of Change
Changes Made
scripts/statusline.mjsentry composing: health (OV ✓/⚠ slow/✗ offline/⚡ bypass), recall (↩ N mem (score) · Yms), capture (✎ X/20k · M arch,✎ committed,✗ N dropped), session events (🔗 resumed/🔗 compact, 1 min TTL), daily archive count (+N today). 80-char hard cap with ANSI-aware truncation;NO_COLOR/OPENVIKING_STATUSLINE_NO_COLOR/TERM=dumbdegrade.auto-recall.mjs/auto-capture.mjs/session-start.mjswrite small JSON snapshots to~/.openviking/state/via atomic temp+rename in newlib/state.mjs. State files:last-recall.json,last-capture.json,last-session-event.json,daily-stats.json.lib/server-probe.mjs: file-cached 5 s shared probe of/healthwith a 1 s hard timeout (originally 250 ms — loosened after spurious offline reports on remote SaaS deployments).setup-helper/install.shaddsregister_statusline()— opt-in[y/N]prompt; detects existing.statusLineand offers replace / skip; idempotent on re-runs; backups~/.claude/settings.json.bak.$tsbefore merge.examples/claude-code-memory-plugin/docs/STATUSLINE.md: segment glossary (every state combo + when each is missing) and personalization recipes (drop a segment, recolor, compose with another statusline, custom segments, reset state). Anchored on$REPO/$PLUGIN/$STATEso an agent resolves all paths from the install location alone.docs/{en,zh}/agent-integrations/02-claude-code.md) gain a refreshed examples block (with⚠ slow,🔗 resumed,+N today) and an absolute GitHub link to STATUSLINE.md (sinceexamples/...is outside the VitePressdocsRoot)..claude-plugin/plugin.jsonversion bump0.2.0→0.3.0.Testing
Manual end-to-end on macOS: opted in via installer, exercised
OV ✓/⚠ slow(unroutable host) /✗ offline(refused port) /⚡ bypass; verified each segment appears and clears against expected hook activity; verified 80-char truncation and ANSI degrade. The statusline is a thin composition over hook-written state and a probe; no unit tests added (smoke-tested vianode scripts/statusline.mjs <<<'{...}').Checklist
pending_tokens,cc_session_idfiltering)Screenshots (if applicable)
Additional Notes
OPENVIKING_STATUSLINE=off(silence without unregistering) orjq 'del(.statusLine)' ~/.claude/settings.json(remove).OV ⚠ slow(yellow) is the dedicated state for probe timeout — distinct fromOV ✗ offline(red) so a slow remote isn't alarmist.