/** * Central lifecycle manager for Senpi skills auto-update. * * Mirrors AutoUpdateCoordinator from src/runtime/auto-update/coordinator.ts * for thread-safety (runToken + stopped + running triple) and uses the same * injectable-dependency pattern for testability. * * Scheduling: this coordinator does not own a timer. Ticks fire from * outside — currently from auto-update's `onTickComplete` hook in * `src/index.ts`, which calls {@link SkillsUpdateCoordinator.runTickOnce} * after every auto-update cycle. Skills-manager therefore inherits the * `autoUpdate.pollIntervalMinutes` cadence and the same failure-backoff * cadence. * * On start(): load persisted state, bootstrap any missing skills, mark * health as healthy. The first tick fires on the next external invocation * of `runTickOnce()`. * * Each tick: take the managed skill set from the manifest * (`skills-manifest.json`, the single source of truth), check remote versions * in parallel, auto-apply patch/minor, log a pending major (never applied or * notified), skip on none/unknown. */ import type { SkillsManagerSnapshot, SkillsUpdateCoordinatorOptions } from "./types.js"; export declare class SkillsCoordinatorUnavailableError extends Error { constructor(message: string); } export declare class SkillsUpdateCoordinator { private readonly options; private readonly store; private operationChain; private stopped; private runToken; private state; private snapshot; /** * Latest remote version per skill name — populated during ticks. Read by * `buildSnapshot` (status RPC) and used as the install `expectedVersion`. */ private latestVersions; constructor(options: SkillsUpdateCoordinatorOptions); /** * Load persisted state and bootstrap missing skills. Does **not** schedule * a poll loop — skills-manager runs off the auto-update coordinator's * `onTickComplete` hook (see `src/index.ts`), which invokes * {@link runTickOnce} on whatever cadence auto-update is configured with. * * Calling `start()` is still required even in the externally-scheduled * mode so bootstrap, state load, and the "ready" snapshot all happen * before the first external tick fires. */ start(): Promise; /** * Stop the coordinator: prevent any new ticks from being processed, and * wait for any in-flight tick or manual RPC to finish so the caller can * rely on no async work still touching state after `stop()` resolves. * * Since scheduling is now external (auto-update's `onTickComplete` hook), * there's no internal timer to cancel — `stop()` just flips the `stopped` * flag (which `runTickOnce` checks) and drains the serialised operation * queue. The auto-update hook itself becomes a no-op for stopped * coordinators since `runTickOnce` returns early. */ stop(): Promise; /** * Public one-shot tick entry point. Called externally on whatever cadence * the host wires up — currently from the auto-update coordinator's * `onTickComplete` hook in `src/index.ts`, which fires after each * auto-update version check completes (success or failure). * * Replaces the previous internal `tick()` + `scheduleNext()` pair: the * coordinator no longer owns a timer of its own. Manual RPCs * (`senpi.skills.update`, `senpi.skills.updateAll`) remain independent of * this path. * * Returns early as a no-op when: * - The coordinator has been stopped (e.g. plugin teardown). * - The runToken has rolled (e.g. a stop/start cycle invalidated this call). * - The coordinator was disabled at start time (config.enabled === false). * * Tick errors are logged + recorded on the snapshot but never thrown — * the caller (auto-update) must not have its own scheduling disturbed by * a transient skills-manager failure. */ runTickOnce(): Promise; /** Return the latest point-in-time snapshot for gateway RPCs. */ getSnapshot(): SkillsManagerSnapshot; /** * Force an immediate version check and apply for one named skill. * * Skips installation when the update kind is `"none"` (already current) or * `"unknown"` (remote version could not be classified — e.g. the fetch * failed or the remote `version` field is missing). Falling through on * `"unknown"` would write `installedVersion: null` and lose tracking, so we * treat it the same as `"none"` and return `updated: false`. * * @throws On network or install error so the gateway RPC can return ok=false. */ updateSkill(skillName: string): Promise<{ updated: boolean; version: string | null; }>; /** * Force an immediate version check and apply for all discovered/configured skills. * Individual failures do not abort the remaining skills. * * All skills share a single `git clone` — the repo is cloned once, every * skill that needs an update is installed from that same checkout, then the * checkout is cleaned up. A previous version called `updateSkill` in a loop, * which caused one full clone per skill (N redundant clones for N skills); * this mirrors the tick path's batching instead. */ updateAll(): Promise>; /** * Run coordinator work in a global critical section so periodic ticks and * manual RPC updates never interleave at await points. */ private runSerialized; /** * Manual gateway RPCs (`updateSkill`, `updateAll`) are only valid while the * coordinator is actively running and enabled. This prevents pre-start calls * (e.g. when SENPI_RUNTIME_API_DISABLED=true skipped start()) from persisting * the constructor-default empty state over a valid on-disk state file. */ private assertManualOperationAllowed; /** * Core tick logic: resolve skill names, check versions in parallel, apply * policy per skill, then run a single clone for all skills needing update. */ private runTick; /** * Clone the repo once and install every skill in `skillNames`. * Per-skill errors are recorded in state without aborting the others; * outcomes are logged (no notification). * * If the clone itself fails we log and return without touching state — a * full-clone failure is almost always a transient network/git issue that * affects every skill equally. The next tick retries. */ private applyUpdates; /** * Clone the remote skills repo exactly once and install every skill in * `skillNames` from that single checkout. Returns a per-skill outcome map: * `{ version }` on success or `{ version: null, error }` on install failure. * * Throws only when the clone itself fails — per-skill install failures are * captured in the map so callers can continue processing the rest. * * When `deps.cloneAndInstall` is supplied (unit tests), that injector is * called in place of the real clone+install loop; any names missing from * its returned map are treated as install failures. */ private cloneAndInstallMany; /** * Record a successful install on in-memory skill state. * * Resets failure tracking — a successful end-to-end install is the only * signal we treat as "operationally healthy." A successful version check * alone is NOT sufficient (and intentionally does not call this), because: * * - The version check only proves the raw SKILL.md is reachable. It does * not exercise the clone/copy/swap pipeline that actually keeps the skill * working. * - Clearing failure state on every check-success would wipe install-failure * history mid-tick: a skill failing to install on every tick would still * report `consecutiveFailures: 1` (instead of N) because each tick's * check-success reset would precede the install-failure increment. * * Consequence: a skill that's perpetually up-to-date but had a one-off * failure in the past will retain a non-zero `consecutiveFailures` until * the next install. We accept that trade-off — surfacing a stale failure * counter is preferable to silently wiping real install-failure history. */ private recordSuccess; /** * Record a version-check failure on in-memory skill state. * * Shared by `runTick` and `updateAll` — both need to bump * `consecutiveFailures` and capture the reason when `Promise.allSettled` * surfaces a rejected check. `lastCheckedAt` is updated too so the snapshot * doesn't look like the skill was never polled (it was — the poll failed). */ private recordCheckFailure; /** * Apply one install outcome to in-memory skill state and return a compact * success/failure result for the caller's bookkeeping. */ private processInstallOutcome; /** * Apply a single skill update (used by the manual `updateSkill` RPC path). * Returns the installed version. * * Delegates to {@link cloneAndInstallMany} so the single-skill and batch * paths share one implementation of clone+install. The per-skill install * error is re-thrown here so `updateSkill` can surface it to the RPC * caller via a rejected promise (batch path captures it instead). */ private applyUpdate; /** * Return the version-check function for this coordinator: either the * injected `deps.checkSkillVersion` (unit tests) or the real * `checkSkillVersion` with the default `fetchImpl`. * * Extracted so the three call sites (`updateSkill`, `updateAll`, `runTick`) * don't repeat the same resolve-and-wrap lambda — a change to the real * `checkSkillVersion` signature now only has to land here. */ private resolveCheckFn; /** * Resolve a skill name to its concrete source (repo/branch/path/rawBaseUrl) * from the manifest, applying any plugin-config per-skill overrides. The * `SENPI_SKILLS_BRANCH` / `SENPI_SKILLS_REPO_URL` env testing knobs are * applied inside {@link resolveSkillSource}. */ private resolveSource; /** * The managed skill set for this tick — **always the manifest** set * (`skills-manifest.json`), the single source of truth. `config.skills` and * directory discovery no longer define the set: the manifest does, full stop * (only `enabled: false` disables the manager entirely). * * `deps.listManagedSkills` is a test-only seam to substitute the set without * a fixture manifest on disk; production always falls through to the * manifest. */ private resolveSkillNames; /** * Build the point-in-time snapshot returned by {@link getSnapshot}. * * The snapshot is the single serialisable view of the coordinator's state * consumed by gateway RPCs (`senpi.skills.status`) and any health UI. It * fuses three data sources: * 1. Per-skill persisted state (`this.state.skills`) — installed version, * last check time, consecutive failures, last failure reason. * 2. In-memory `this.latestVersions` map — the most recent remote version * observed during a version check (may not yet be installed). * 3. Caller-supplied lifecycle metadata — `health`, `lastAction`, * `lastError` describing the most recent tick / manual RPC outcome. * * This is called at every state-mutating boundary (start, tick complete, * tick error, manual update complete, manual update skipped, manual update * clone failed, stop/disable) so the next RPC response reflects the latest * state rather than stale pre-action data. */ private buildSnapshot; private persistState; } //# sourceMappingURL=coordinator.d.ts.map