Files
2026-08-24 11:09:03 +00:00

169 lines
8.5 KiB
Markdown

---
status: implemented
created: 2026-08-02
owner: cfl
spec: docs/specs/plugins/requirements/plugins.md
---
# Plugin reliability repair
## Goal
Make plugin failures recoverable and diagnosable from Settings, and make the
event-buffer overflow signal useful under sustained failure. The repair covers
the installed-plugin symptoms found in `~/kandev/logs.txt`: persisted `error`
states with no reason, an unavailable retry action, and one warning per dropped
buffered event. This remediation also closes the review regressions around
concurrent recovery, diagnostic secrecy, authoritative retry refresh, and phone
layout safety.
## Evidence and root cause
- The backend state machine already permits `error -> active`, but both plugin
settings surfaces only offer **Enable** for `registered` and `disabled`.
- The plugin record persists status but no failure reason or timestamp, so a
restart leaves the operator with an opaque `Error` badge.
- Runtime health callbacks carry only healthy/unhealthy state, so health and
restart failures cannot be persisted with their originating error.
- The delivery ring buffer intentionally retains `error` plugins, but its
overflow path logs every dropped event. The current log contains a sustained
stream of Kandy overflow warnings.
- Restart exhaustion publishes `gaveUp` before its final synchronous status
callback returns. A concurrent `claimStart` holds `Manager.mu` while
`p.stop()` waits for that callback, which can re-enter `RestartCount` and wait
forever on `Manager.mu`. A barrier repro in the runtime package fails on the
current code.
- `handleEnable` catches a failed POST without refetching the backend record, so
the store keeps the previous `last_error` even though the service persisted a
replacement diagnostic.
- `normalizePluginError` only collapses whitespace and truncates raw
`err.Error()`, allowing plugin stdout and credential-like subprocess details to
become durable API data.
- Phone recovery controls use `min-h-9` (36px), and the diagnostic container has
no wrapping rule for an unbroken bounded token.
## Behavioral changes
1. Persist a bounded, single-line `last_error` (maximum 2048 bytes) and UTC
`last_error_at` for failed spawn, handshake, health, restart, and install-path
checks. Do not persist configuration values or secrets. Clear both fields
after a successful handshake and health check.
2. Include those nullable fields in the existing plugin list/detail API response.
Keep old records compatible when the fields are absent.
3. Treat `error` as a recoverable state in both desktop and mobile plugin
settings: show the diagnostic when present and expose **Enable** as the manual
retry action. A failed retry replaces the diagnostic; a successful retry clears
it. Boot must not automatically retry persisted `error` records.
4. Keep ring-buffer capacity and TTL unchanged. Emit at most one overflow warning
per plugin per 60 seconds and include the number of drops aggregated since the
previous warning.
5. A manual Enable concurrent with restart exhaustion must not deadlock. The
manager must not hold its registry mutex while waiting for a process to stop;
the final exhaustion callback and replacement start must both be allowed to
finish.
6. Failed Enable must refetch the authoritative plugin record and upsert it so a
replacement diagnostic is visible immediately. Recovery controls must be at
least 44px high on phone layouts, and diagnostic text must wrap without page
overflow.
7. Persisted diagnostics must redact PATs, bearer tokens, labeled secrets, and
host home paths before the 2048-byte bound is applied.
## Implementation design
### Backend failure diagnostics
- Extend `plugins/store.Record` with nullable `LastError` and `LastErrorAt`
fields, preserving YAML/JSON compatibility for existing records.
- Add registry/service helpers that atomically update status and diagnostic data,
roll back the in-memory record when persistence fails, and normalize errors to a
safe single-line bounded message. Redact credential-like values and home paths
before truncation.
- Change runtime status callbacks to carry the triggering error for unhealthy
transitions. Pass the ping/restart/exit cause through `runtime.Manager` and
`runtime.Process`; healthy callbacks clear the diagnostic.
- Record failures from initial activation, boot activation, config-change restart,
health transitions, restart exhaustion, and missing install paths. Preserve the
last diagnostic while disabled; clear it only on successful recovery.
- Keep `Manager.mu` out of process-stop waits and add a barrier regression test
covering final restart exhaustion against concurrent manual Enable.
- Add unit tests for serialization compatibility, truncation/newline handling,
every service transition, and runtime callback propagation.
### Delivery overflow diagnostics
- Add a per-worker overflow log accumulator with an injectable clock. The first
drop logs immediately; subsequent drops within the 60-second interval are
counted without logging; the first drop after the interval logs the aggregate
count and starts a new interval.
- Keep the existing oldest-drop behavior, TTL purge, recovery flush, delivery
ordering, and queue semantics unchanged.
- Add deterministic delivery tests asserting warning counts and `dropped_count`
aggregation for each plugin independently.
### Frontend recovery
- Allow `error` in `canEnable` for both `PluginRow` and `PluginDetail`.
- Render a compact, accessible diagnostic region from `last_error` in the row and
detail views without adding a new untranslated label; preserve the existing
status badge and responsive action layout.
- Clear diagnostic fields in the optimistic enable action so the UI does not show
stale failure text after a successful retry.
- On failed Enable, fetch `GET /api/plugins/{id}` with `no-store` and upsert the
returned record before showing the toast.
- Use `min-h-11 sm:min-h-0` for phone recovery controls and add wrapping to the
diagnostic region.
- Add component/action tests and a Playwright recovery flow. Exercise the same
settings path at a phone viewport to preserve mobile parity.
### Documentation
- Update `docs/public/plugins.md` with the error-state retry and diagnostic
behavior, and the aggregated overflow-warning policy.
- Run the public-doc validation scripts and the targeted backend/frontend tests.
## Task order
1. `task-01-backend-diagnostics.md` — record contract, runtime propagation, and
lifecycle persistence.
2. `task-02-overflow-logging.md` — per-plugin rate-limited aggregation and tests.
3. `task-03-frontend-recovery.md` — retry action, diagnostics, mobile coverage,
and frontend tests.
4. `task-04-docs-and-verification.md` — public docs and final targeted/full gates.
Tasks 01 and 02 are backend-only and can be implemented sequentially in this
session. Task 03 depends on the API fields from task 01. Task 04 follows the
behavioral implementation.
## Acceptance criteria
- A failed plugin record exposes a useful, bounded `last_error` and timestamp
after backend restart; a successful manual Enable clears both.
- An errored plugin can be retried from both desktop and mobile settings, and a
failed retry remains visibly errored with the new reason after an authoritative
refetch; the runtime deadlock regression test completes under a barrier.
- Persisted diagnostic tests prove PAT, bearer, labeled-secret, and home-path
redaction, while retaining useful non-secret error context.
- Phone recovery targets measure at least 44px and a long diagnostic does not
create horizontal overflow.
- Sustained ring-buffer overflow produces no more than one warning per plugin per
minute, with the suppressed-drop count reported on the next warning.
- Existing lifecycle, buffering, delivery, install, and plugin settings tests
remain green; public plugin docs describe the new operator behavior.
## Out of scope
- Automatic boot retry for persisted `error` records.
- Changing the 100-event/5-minute ring-buffer policy or making the buffer durable.
- Per-plugin event admission rate limiting, package signing, or unrelated plugin
startup failures.
## Validation
- Backend targeted tests for `internal/plugins/store`, `internal/plugins`,
`internal/plugins/runtime`, and `internal/plugins/delivery`.
- Web plugin component/action tests and typecheck/lint.
- Plugin Playwright tests, including a phone viewport recovery flow.
- `node --test scripts/validate-public-docs.test.mjs` and
`node scripts/validate-public-docs.mjs`.
- Required backend format/typecheck/test/lint gates before handoff.