Files
oc-ls-stats/AGENTS.md
T
Troed Sångberg 36fcaa27a1 feat: v0.0.40 - per-slot PP calculation from /slots endpoint
- Fix PP calculation: use per-slot n_prompt_tokens during prefill state
  instead of global llamacpp:prompt_tokens_total from /metrics
- Add isPrefilling flag and prefill tracking state (slotId, capturedTokens, startAt)
- Add dedicated formatters: formatPps, formatTps, formatAvg, pad4
- Replace formatRate suffixes with raw number formatters
- Display: PP | TPS | AVG with fixed-width 4-char padding
- Slot polling every 2s with model query parameter
- Debug logging to /tmp/oc-ls-stats-debug.log
- Add .npmignore, TUIPLUGIN.md, test-debug-log.jsonl
- Update AGENTS.md with slot state phases, PP calculation docs
2026-06-13 22:52:31 +02:00

248 lines
9.9 KiB
Markdown

# AGENTS.md
## Structure
- **`tui.tsx`** — only source file. Exports a `TuiPluginModule` as default. No build step; OpenCode compiles JSX at runtime.
- **`package.json`** — declares peer deps (`@opencode-ai/plugin`, `@opentui/core`, `@opentui/solid`, `solid-js`) and an `engines.opencode` constraint (`>=1.3.14`). The `exports` field maps `./tui` to `tui.tsx`.
## Constraints
- No tsconfig, no test runner, no lint/format.
- `node_modules` is gitignored.
- JSX uses `@opentui/solid` (`/** @jsxImportSource @opentui/solid */`).
- The plugin registers via `api.slots.register` to render into `session_prompt_right`.
## TUI Plugin Loading
OpenCode has TWO separate plugin loaders:
- **Server plugins** — configured in `opencode.jsonc`
- **TUI plugins** — configured in `~/.config/opencode/tui.json` under the `plugin` field
Server plugins in `opencode.jsonc` do NOT load TUI plugins. TUI plugins must be listed in `tui.json`:
```json
{"plugin": ["oc-ls-stats@latest"]}
```
OpenCode's `file://` path loader does not resolve `package.json` exports field for TUI plugins. TUI plugins require npm-style module resolution (package published to npm registry or private npm-compatible registry like Forgejo).
### Important: Plugin Installation
- **npm plugins are installed using Bun at startup**, not npm
- Cached in `~/.cache/opencode/node_modules/`
- `~/.npmrc` is respected for private registry auth (Bun's native `.npmrc` support)
- For private registries, ensure `~/.npmrc` has the correct format:
```
registry=https://your-registry/api/npm/
//your-registry/api/npm/:_authToken=YOUR_TOKEN
```
### tui.json Format Limitations
- Only plain strings are supported: `["plugin-name@latest"]`
- **Tuple format with options is NOT supported**: `["plugin", { "url": "..." }]` will be rejected
- Log shows: `skipping invalid tui config — Expected string | array, got {"url":"..."}`
- To pass config to TUI plugins, hardcode in the plugin code or use environment variables
### Debugging Plugin Loading Issues
- **Check logs**: `~/.local/share/opencode/log/` (most recent: `opencode.log`)
- **Plugin loading errors**: grep for `skipping invalid tui config` or `plugin`
- **Run with debug**: `opencode --log-level DEBUG` or `opencode --print-logs`
- **Clear cache**: `rm -rf ~/.cache/opencode` (removes all plugin caches)
- **Verify installed version**: check `~/.cache/opencode/packages/<plugin>@latest/package.json` for pinned version
## Prefill Tracking (llama-server)
The plugin polls llama-server's `GET /slots` endpoint to track prompt processing (prefill) timing.
### URL Discovery
- Falls back to reading OpenCode config from `api.state.path.config`
- Parses JSONC (strips `//`, `/* */` comments and trailing commas)
- Extracts `baseURL`/`base_url` from `provider.*.options` where value contains "localhost"
- Strips `/v1` path suffix to get the llama-server base URL
- Falls back to `http://localhost:8080` default URL
### Slot Polling
- Polls `GET /slots?model=...` every 2 seconds (`SLOT_POLL_MS = 2000`)
- Prefill detection: `is_processing === true` AND `next_token[0].n_remain === -1`
- Model parameter is required by the `/slots` endpoint
- Falls back to `http://localhost:8080` default URL
### Display Format
`PP {pp} | TPS {tps} | AVG {avg}`
Example: `PP 1247 | TPS 25 | AVG 25`
### Display Rules
- PP: integer only, no suffix, 4-char fixed-width padded
- TPS: integer only, 4-char fixed-width padded
- AVG: one decimal when < 100, integer when >= 100, 4-char fixed-width padded
- "----" shown when no value available
### Limitations
- Slot state tracked globally (not per-session) — slot IDs don't map to OpenCode sessions
- Shows latest prefill data across all slots
- Requires llama-server to have `/slots` endpoint enabled (default)
## Deployment
- Forgejo npm registry: `https://git.sync.wtf/api/packages/troed/npm/`
- Package name: `oc-ls-stats` (llama-server stats, distinct from `oc-tps` on npmjs.org)
- Auth: token in `~/.npmrc`
- Git remotes: `origin` → Forgejo, `upstream` → GitHub
- To update: bump version in `package.json`, run `npm publish --registry https://git.sync.wtf/api/packages/troed/npm/`, clear cache (`rm -rf ~/.cache/opencode/packages/@troed`), restart OpenCode
## Debugging: Plugin Not Loading / No Output
### Symptom
Plugin installs from private registry but produces no output in OpenCode UI.
### Root Causes Identified
#### 1. Incorrect TUI Function Signature (CRITICAL)
**Error:** Type mismatch prevented plugin from loading.
```typescript
// ❌ WRONG - causes load failure
const tui: TuiPlugin = async (api, options) => { ... }
// ✅ CORRECT
const tui: TuiPlugin = async (api) => { ... }
```
The `TuiPlugin` type from `@opencode-ai/plugin/tui` expects exactly one parameter: `api`.
#### 2. Malformed Regex in JSONC Parser
**Error:** Syntax error in character class regex.
```typescript
// ❌ WRONG - invalid regex syntax
text.indexOf(/[\s}])/), i + 1)
/[\}])]/.test(text[next])
// ✅ CORRECT
text.indexOf(/[\s}\]]/, i + 1)
/[\s}\]]/.test(text[next])
```
Regex literals with unescaped parentheses caused JavaScript parse errors.
#### 3. Hardcoded Test Text Replaced Component
**Error:** Development debug code left in production.
```tsx
// ❌ WRONG - no actual output
return <text fg="#ff0000" bg="#000000">*** SCOPED v0.0.17 LOADED ***</text>
// ✅ CORRECT
return <SessionPromptRight api={api} sessionID={value.session_id} tracker={tracker} version={version} clock={clock} />
```
#### 4. Extra Export in package.json
**Error:** Non-standard exports configuration.
```json
// ❌ WRONG - extra root export
{
"exports": {
".": { "import": "./tui.tsx" },
"./tui": { "import": "./tui.tsx" }
}
}
// ✅ CORRECT - match working plugin config
{
"exports": {
"./tui": { "import": "./tui.tsx" }
}
}
```
### Verification Steps
1. **Check plugin loads:** Look for `[oc-ls-stats] TUI plugin loaded!` in OpenCode logs
2. **Clear cache between tests:** `rm -rf ~/.cache/opencode`
3. **Verify installed version:** `cat ~/.cache/opencode/packages/@troed/oc-ls-stats@latest/node_modules/@troed/oc-ls-stats/package.json`
4. **Compare with working plugin:** Diff against cached `@troed/oc-tps@latest` to spot discrepancies
### Resolution Timeline
- Confirmed OpenCode CAN load scoped packages from Forgejo (tested with `@troed/oc-tps`)
- Compared working `@troed/oc-tps` vs non-working `@troed/oc-ls-stats` line-by-line
- Fixed all 4 issues above in version 0.0.20
- Published 0.0.20 to Forgejo registry
## Prefill Rate (PP) Calculation — v0.0.40+
### Slot State Structure (from `GET /slots?model=...`)
```json
{
"id": 2,
"n_ctx": 160000,
"speculative": false,
"is_processing": true,
"n_prompt_tokens": 12345,
"n_prompt_tokens_processed": 12345,
"next_token": {
"has_next_token": true,
"has_new_line": false,
"n_remain": -1,
"n_decoded": 0
}
}
```
### Slot State Phases
| Phase | `is_processing` | `n_remain` | `n_decoded` | `n_prompt_tokens` |
|-------|-----------------|------------|-------------|-------------------|
| Idle | `false` | — | — | stable |
| Prefill | `true` | `-1` | `0` | incrementing |
| Transition | `true` | `>0` (e.g. 31999) | `0` | stable (final) |
| Generation | `true` | `>0` (decreasing) | `>0` (incrementing) | stable (final) |
**Key insight:** `n_remain === -1` is the definitive prefill indicator. `n_decoded === 0` alone is not sufficient — a slot can have `n_decoded === 0` during generation start (transition phase).
### PP Calculation Method
PP is calculated from the per-slot `n_prompt_tokens` field during prefill:
1. Detect prefill: slot with `is_processing === true` AND `next_token[0].n_remain === -1`
2. On first detection: capture `slot.id` and `slot.n_prompt_tokens` as baseline
3. On subsequent polls: `delta = current_n_prompt_tokens - baseline_tokens`, `dt = (now - startAt) / 1000`, `PP = delta / dt`
4. Reset when no slot has `n_remain === -1` (prefill ended)
### Why NOT `llamacpp:prompt_tokens_total` from `/metrics`
`llamacpp:prompt_tokens_total` is a **global counter across all slots**, not per-model. When multiple slots are active simultaneously (e.g., slot A prefilling while slot B generating), the delta includes tokens from all slots, producing wildly inflated PP values.
**Example from debug data (`test-debug-log.jsonl`):**
- Line 15: slot 3 prefilling, `prompt_tokens_total` delta = 49 tokens in 1815ms → PP = 27 (correct but tiny delta)
- Line 31: slot 2 prefilling, slot 3 still generating, `prompt_tokens_total` delta = 13793 tokens in 668ms → PP = 20,648 (wrong — includes slot 3 tokens)
- Actual server console showed ~1200-1300 tokens/sec during prefill
### Why NOT `llamacpp:prompt_tokens_seconds` from `/metrics`
`llamacpp:prompt_tokens_seconds` is an **average since server start**, not an instantaneous rate. It smooths out all prefill events into a single running average, making it useless for displaying current prefill speed.
### Slot Polling
- Polls `GET /slots?model=...` every 2 seconds (`SLOT_POLL_MS = 2000`)
- Model parameter is required by the `/slots` endpoint (returns error without it)
- Model discovered from: `tui.json` options → route session → OpenCode config provider parsing
- Falls back to `http://localhost:8080` default URL
### Display
PP only shown during active prefill (`n_remain === -1`). Format: integer only, no suffix (e.g., "1247"). Fixed-width 4 chars, space-padded. Shows "----" when no value.
## Debugging: PP Value Wrong
### Symptom
PP shows 3634 when llama-server console shows ~1200-1300 tokens/sec during prefill.
### Root Cause (fixed in v0.0.40)
Used `llamacpp:prompt_tokens_total` from `/metrics` endpoint which is a global counter across all slots. Delta between polls included tokens from other active slots.
### Fix
Track `n_prompt_tokens` from the slot data itself (per-slot field) during prefill state (`n_remain === -1`).
### Debug Log Location
`/tmp/oc-ls-stats-debug.log` — TUI plugin writes here because `console.log` in TUI doesn't reach main OpenCode log.
### Debug Data
Sample debug log with slot states and metrics values: `test-debug-log.jsonl`