@vikrant82/opencode-cache-keepalive 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +255 -56
- package/dist/index.js.map +1 -1
- package/dist/lib/keepalive.d.ts +50 -8
- package/dist/lib/keepalive.d.ts.map +1 -1
- package/dist/lib/state.d.ts +45 -5
- package/dist/lib/state.d.ts.map +1 -1
- package/lib/keepalive.ts +323 -77
- package/lib/state.ts +48 -5
- package/package.json +5 -3
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../lib/auth.ts","../lib/config.ts","../lib/control.ts","../lib/paths.ts","../lib/model.ts","../lib/keepalive.ts","../lib/logger.ts","../lib/state.ts","../lib/system.ts","../index.ts"],"sourcesContent":["/**\n * When opencode runs with `OPENCODE_SERVER_PASSWORD`, the local HTTP API requires\n * Basic auth. The SDK client handed to plugins does not carry it automatically,\n * so we attach an interceptor. No-op when the server is not password protected.\n *\n * Same approach as opencode-dynamic-context-pruning's auth helper.\n */\nexport function isSecureMode(): boolean {\n return !!process.env.OPENCODE_SERVER_PASSWORD\n}\n\nexport function getAuthorizationHeader(): string | undefined {\n const password = process.env.OPENCODE_SERVER_PASSWORD\n if (!password) return undefined\n const username = process.env.OPENCODE_SERVER_USERNAME ?? \"opencode\"\n const credentials = Buffer.from(`${username}:${password}`).toString(\"base64\")\n return `Basic ${credentials}`\n}\n\nexport function configureClientAuth(client: any): void {\n const header = getAuthorizationHeader()\n if (!header) return\n\n const inner = client?._client ?? client?.client\n if (!inner?.interceptors?.request) return\n\n inner.interceptors.request.use((request: Request) => {\n if (!request.headers.has(\"Authorization\")) {\n request.headers.set(\"Authorization\", header)\n }\n return request\n })\n}\n","export type KeepaliveConfig = {\n /** Master switch. When false the plugin registers no timers or hooks. */\n enabled: boolean\n /** Milliseconds between pings. Must stay under the shortest supported provider TTL. */\n intervalMs: number\n /**\n * How long to keep warming after the last real response before giving up and\n * letting the cache go cold. The default follows the Copilot Claude\n * write/read break-even; providers with automatic caching may be cheaper.\n */\n windowMs: number\n /**\n * Append a stable keepalive instruction to the system prompt so the model\n * answers a `~` ping with a single `~` token and never calls tools. The\n * instruction is added to real turns too, so the cached prefix stays identical.\n */\n injectSystemInstruction: boolean\n /** The single character/token used as the keepalive suffix. */\n pingToken: string\n /** Remove synthetic ping turns after measuring their cache usage. */\n revertPing: boolean\n /** Warm subagent/child sessions too. Off by default to avoid wasted pings. */\n includeChildSessions: boolean\n debug: boolean\n /** providerID substrings eligible for warming (cache-controlled providers). */\n providerAllowlist: string[]\n /** modelID substrings eligible for warming. */\n modelAllowlist: string[]\n}\n\nconst DEFAULTS: KeepaliveConfig = {\n enabled: true,\n intervalMs: 270_000, // 4.5 min — below the shortest supported cache TTL\n windowMs: 3_300_000, // 55 min — the Copilot Claude write/read break-even\n injectSystemInstruction: true,\n pingToken: \"~\",\n revertPing: true,\n includeChildSessions: false,\n debug: false,\n providerAllowlist: [\"copilot\"],\n modelAllowlist: [\"claude\", \"anthropic\", \"sonnet\", \"opus\", \"haiku\", \"gpt\"],\n}\n\nexport function getConfig(options: Record<string, unknown> | undefined): KeepaliveConfig {\n const o = options ?? {}\n const env = process.env\n\n return {\n enabled: boolOpt(o.enabled, env.OPENCODE_KEEPALIVE_ENABLED !== \"false\" && DEFAULTS.enabled),\n intervalMs: durationOpt(\n o.intervalMs,\n o.intervalSeconds,\n numEnv(env.OPENCODE_KEEPALIVE_INTERVAL_MS, DEFAULTS.intervalMs),\n ),\n windowMs: durationOpt(\n o.windowMs,\n o.windowMinutes ? Number(o.windowMinutes) * 60 : undefined,\n numEnv(env.OPENCODE_KEEPALIVE_WINDOW_MS, DEFAULTS.windowMs),\n ),\n injectSystemInstruction: boolOpt(\n o.injectSystemInstruction,\n DEFAULTS.injectSystemInstruction,\n ),\n pingToken:\n typeof o.pingToken === \"string\" && o.pingToken.length > 0\n ? o.pingToken\n : DEFAULTS.pingToken,\n revertPing: boolOpt(\n o.revertPing,\n env.OPENCODE_KEEPALIVE_REVERT_PING !== \"false\" && DEFAULTS.revertPing,\n ),\n includeChildSessions: boolOpt(o.includeChildSessions, DEFAULTS.includeChildSessions),\n debug: boolOpt(o.debug, env.OPENCODE_KEEPALIVE_DEBUG === \"true\"),\n providerAllowlist: listOpt(o.providerAllowlist, DEFAULTS.providerAllowlist),\n modelAllowlist: listOpt(o.modelAllowlist, DEFAULTS.modelAllowlist),\n }\n}\n\nfunction boolOpt(value: unknown, fallback: boolean): boolean {\n return typeof value === \"boolean\" ? value : fallback\n}\n\nfunction listOpt(value: unknown, fallback: string[]): string[] {\n if (!Array.isArray(value)) return fallback\n const filtered = value.filter((item): item is string => typeof item === \"string\")\n return filtered.length > 0 ? filtered : fallback\n}\n\n/** Accept an explicit `*Ms` option, a `*Seconds` option, or fall back. */\nfunction durationOpt(ms: unknown, seconds: unknown, fallback: number): number {\n if (typeof ms === \"number\" && Number.isFinite(ms) && ms > 0) return ms\n if (typeof seconds === \"number\" && Number.isFinite(seconds) && seconds > 0)\n return seconds * 1000\n return fallback\n}\n\nfunction numEnv(raw: string | undefined, fallback: number): number {\n if (!raw) return fallback\n const value = Number(raw)\n return Number.isFinite(value) && value > 0 ? value : fallback\n}\n","import { readFileSync } from \"node:fs\"\nimport { mkdir, rename, writeFile } from \"node:fs/promises\"\nimport { dirname } from \"node:path\"\nimport { controlFilePath } from \"./paths\"\n\nexport type KeepaliveControl = {\n version: 1\n enabled: boolean\n updatedAt: number\n}\n\nexport function readControl(directory: string): KeepaliveControl | undefined {\n try {\n const value = JSON.parse(\n readFileSync(controlFilePath(directory), \"utf8\"),\n ) as Partial<KeepaliveControl>\n if (value.version !== 1 || typeof value.enabled !== \"boolean\") return undefined\n if (typeof value.updatedAt !== \"number\") return undefined\n return value as KeepaliveControl\n } catch {\n return undefined\n }\n}\n\nexport async function writeControl(enabled: boolean, directory: string): Promise<KeepaliveControl> {\n const value: KeepaliveControl = {\n version: 1,\n enabled,\n updatedAt: Date.now(),\n }\n const path = controlFilePath(directory)\n const tmp = `${path}.${process.pid}.${value.updatedAt}.tmp`\n await mkdir(dirname(path), { recursive: true })\n await writeFile(tmp, `${JSON.stringify(value)}\\n`, \"utf8\")\n await rename(tmp, path)\n return value\n}\n","import { createHash } from \"node:crypto\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\n\n/**\n * Absolute path to the JSON state file shared between the server plugin (writer)\n * and the TUI plugin (reader). Both processes run on the same machine, so a file\n * under opencode's plugin storage dir is the simplest cross-process channel.\n *\n * Mirrors the storage layout used by other opencode plugins:\n * <XDG_DATA_HOME | ~/.local/share>/opencode/storage/plugin/keepalive/state.json\n */\nexport function stateFilePath(directory: string): string {\n const key = createHash(\"sha256\").update(directory).digest(\"hex\").slice(0, 16)\n return join(stateDirectoryPath(), `state-${key}.json`)\n}\n\nexport function stateDirectoryPath(): string {\n return join(\n process.env.XDG_DATA_HOME ?? join(homedir(), \".local\", \"share\"),\n \"opencode\",\n \"storage\",\n \"plugin\",\n \"keepalive\",\n )\n}\n\n/**\n * Runtime on/off control written by the TUI and read by the server plugin.\n * Scoped per directory (like the state file) so toggling keepalive in one\n * project does not affect every other open opencode session.\n */\nexport function controlFilePath(directory: string): string {\n const key = createHash(\"sha256\").update(directory).digest(\"hex\").slice(0, 16)\n return join(stateDirectoryPath(), `control-${key}.json`)\n}\n\n/** Append-only log file for the server plugin — never stdout, which corrupts the TUI. */\nexport function logFilePath(): string {\n return join(stateDirectoryPath(), \"server.log\")\n}\n","import type { KeepaliveConfig } from \"./config\"\n\n/**\n * Warming only helps on providers that do prefix caching we can refresh by\n * replaying the prefix. Supported defaults include Claude and GPT models served\n * through GitHub Copilot. Everything else is skipped.\n */\nexport function isEligibleModel(\n config: KeepaliveConfig,\n providerID: string | undefined,\n modelID: string | undefined,\n): boolean {\n if (!providerID || !modelID) return false\n const provider = providerID.toLowerCase()\n const model = modelID.toLowerCase()\n const providerOk = config.providerAllowlist.some((needle) => provider.includes(needle))\n const modelOk = config.modelAllowlist.some((needle) => model.includes(needle))\n return providerOk && modelOk\n}\n","import type { KeepaliveConfig } from \"./config\"\nimport { readControl } from \"./control\"\nimport { isEligibleModel } from \"./model\"\nimport type { KeepaliveStore, SessionKeepalive } from \"./state\"\nimport type { Logger } from \"./logger\"\n\nconst TICK_MS = 15_000\nconst JITTER_MS = 15_000\nconst CONTROL_POLL_MS = 1_000\nconst STATUS_POLL_MS = 2_000\nconst PING_STATUS_GRACE_MS = 5_000\n/** Backstop: clear a stuck `busy` flag after this long without an idle event. */\nconst BUSY_TIMEOUT_MS = 300_000\n\n/**\n * The keepalive engine.\n *\n * Lifecycle per session:\n * 1. A real turn finishes -> `session.idle` -> arm a warm window and schedule pings.\n * 2. On each tick past `nextPingAt` (and within the window) -> `firePing`.\n * 3. `firePing` sends a `~` prompt through opencode (exact cached prefix, reuses\n * the session's model/auth), reads `usage` to confirm a cache hit, then optionally\n * reverts the `~`/`~` turn so it does not pollute the real conversation.\n * 4. A new real user turn or the window closing stops warming.\n */\nexport class KeepaliveEngine {\n private timer: ReturnType<typeof setInterval> | undefined\n private controlTimer: ReturnType<typeof setInterval> | undefined\n private statusTimer: ReturnType<typeof setInterval> | undefined\n private controlUpdatedAt = 0\n private reconcilingStatus = false\n private enabled: boolean\n\n constructor(\n private readonly client: any,\n private readonly config: KeepaliveConfig,\n private readonly store: KeepaliveStore,\n private readonly logger: Logger,\n private readonly directory: string,\n ) {\n const control = readControl(directory)\n this.enabled = control?.enabled ?? config.enabled\n this.controlUpdatedAt = control?.updatedAt ?? 0\n this.store.setEnabled(this.enabled)\n }\n\n start(): void {\n if (this.timer) return\n this.timer = setInterval(() => void this.tick(), TICK_MS)\n this.controlTimer = setInterval(() => this.pollControl(), CONTROL_POLL_MS)\n this.statusTimer = setInterval(() => void this.reconcileStatus(), STATUS_POLL_MS)\n this.store.persist()\n this.logger.info(\n `started (${this.enabled ? \"enabled\" : \"disabled\"}, ` +\n `interval=${Math.round(this.config.intervalMs / 1000)}s, ` +\n `window=${Math.round(this.config.windowMs / 60_000)}m)`,\n )\n }\n\n stop(): void {\n if (!this.timer) return\n clearInterval(this.timer)\n this.timer = undefined\n if (this.controlTimer) clearInterval(this.controlTimer)\n this.controlTimer = undefined\n if (this.statusTimer) clearInterval(this.statusTimer)\n this.statusTimer = undefined\n }\n\n /** True while a ping is in flight — used to block tool calls during a ping. */\n isWarming(sessionID: string): boolean {\n return !!this.store.get(sessionID)?.warming\n }\n\n async onEvent(event: any): Promise<void> {\n const type: string | undefined = event?.type\n if (!type) return\n\n if (type === \"session.deleted\") {\n const id = event.properties?.sessionID ?? event.properties?.info?.id\n if (id) {\n this.store.remove(id)\n this.store.persist()\n }\n return\n }\n\n if (type === \"message.updated\") {\n const info = event.properties?.info\n const sessionID: string | undefined = info?.sessionID ?? event.properties?.sessionID\n if (!sessionID) return\n const session = this.store.get(sessionID)\n // Ignore everything that happens inside our own ping turn.\n if (session?.warming) return\n const eventAt = info?.time?.completed ?? info?.time?.created\n // Events are asynchronous: a completed ping may still emit message updates\n // after `warming` clears. Their timestamps predate the recorded ping.\n if (session?.lastPingAt && typeof eventAt === \"number\" && eventAt <= session.lastPingAt)\n return\n\n if (info?.role === \"user\") {\n const s = this.store.ensure(sessionID)\n s.busy = true\n s.busySince = Date.now()\n s.active = false\n this.store.persist()\n return\n }\n\n if (info?.role === \"assistant\" && typeof info?.time?.completed === \"number\") {\n const s = this.store.ensure(sessionID)\n s.lastResponseAt = info.time.completed\n this.store.persist()\n }\n return\n }\n\n if (type === \"session.status\") {\n const sessionID: string | undefined = event.properties?.sessionID\n const status: string | undefined = event.properties?.status?.type\n if (!sessionID || !status) return\n\n // The ping and its optional revert emit status transitions after their\n // requests resolve. Ignore them briefly so they cannot re-arm/reset the\n // active warm window. Real user-message events still stop warming now.\n const current = this.store.get(sessionID)\n if (\n current?.warming ||\n (status !== \"idle\" &&\n current?.lastPingAt &&\n Date.now() - current.lastPingAt < PING_STATUS_GRACE_MS)\n )\n return\n\n if (status === \"idle\") {\n await this.armWindow(sessionID)\n return\n }\n\n const s = this.store.ensure(sessionID)\n s.busy = true\n s.busySince = Date.now()\n s.active = false\n this.store.persist()\n return\n }\n\n // Compatibility fallback for opencode versions predating session.status.\n if (type === \"session.idle\") {\n const sessionID: string | undefined = event.properties?.sessionID\n if (!sessionID) return\n if (this.store.get(sessionID)?.warming) return // idle from our own ping\n await this.armWindow(sessionID)\n }\n }\n\n private async armWindow(sessionID: string): Promise<void> {\n const s = this.store.ensure(sessionID)\n // Duplicate idle events (including late events from a ping) must not reset the\n // current idle stretch or extend its warm window.\n if (s.active && !s.busy) return\n // Serialize: only one armWindow may resolve metadata at a time per session.\n // This prevents the session.status + session.idle double-event race from\n // producing two concurrent resolveSession calls that can clobber each other.\n if (s.arming) return\n s.arming = true\n try {\n s.busy = false\n s.busySince = undefined\n await this.resolveSession(s)\n\n // A new turn may have started while session metadata was being resolved.\n if (s.busy || !this.enabled || !s.eligible) {\n s.active = false\n this.store.persist()\n return\n }\n\n const now = Date.now()\n if (!s.lastResponseAt) s.lastResponseAt = now\n s.idleSince = now\n s.windowEndsAt = now + this.config.windowMs\n s.nextPingAt = now + this.config.intervalMs\n s.pingsSent = 0\n s.lastPing = undefined\n s.active = true\n this.store.persist()\n this.logger.dbg(`armed ${short(sessionID)} model=${s.modelLabel}`)\n } finally {\n s.arming = false\n }\n }\n\n private async tick(): Promise<void> {\n if (!this.enabled) return\n const now = Date.now()\n for (const s of this.store.all()) {\n // Backstop: if busy is stuck (missed idle event + reconcile failure),\n // clear it after BUSY_TIMEOUT_MS so the session can re-arm.\n if (s.busy && s.busySince && now - s.busySince > BUSY_TIMEOUT_MS) {\n this.logger.dbg(`busy timeout ${short(s.sessionID)} — clearing stuck flag`)\n s.busy = false\n s.busySince = undefined\n void this.armWindow(s.sessionID)\n continue\n }\n\n if (!s.eligible || !s.active || s.busy || s.warming) continue\n\n if (now >= s.windowEndsAt) {\n s.active = false\n this.store.persist()\n this.logger.dbg(\n `window closed ${short(s.sessionID)} after ${s.pingsSent} ping(s) — cache may cool`,\n )\n continue\n }\n\n if (now >= s.nextPingAt) void this.firePing(s.sessionID)\n }\n }\n\n private async firePing(sessionID: string): Promise<void> {\n const s = this.store.get(sessionID)\n if (!s || s.warming || s.busy) return\n\n s.warming = true\n // Count every request attempt. A transport/provider failure still consumed a\n // scheduled ping slot and must remain visible in the footer.\n s.pingsSent += 1\n this.store.persist()\n const startAt = Date.now()\n\n try {\n const res = await this.client.session.prompt({\n path: { id: sessionID },\n body: { parts: [{ type: \"text\", text: this.config.pingToken }] },\n })\n\n const info = res?.data?.info ?? res?.info\n const cache = info?.tokens?.cache ?? {}\n const record = {\n at: Date.now(),\n hit: Number(cache.read ?? 0) > 0,\n input: Number(info?.tokens?.input ?? 0),\n cacheRead: Number(cache.read ?? 0),\n cacheWrite: Number(cache.write ?? 0),\n output: Number(info?.tokens?.output ?? 0),\n }\n s.lastPing = record\n this.logger.dbg(\n `ping ${short(sessionID)} ${record.hit ? \"HIT\" : \"MISS\"} ` +\n `in=${record.input} read=${record.cacheRead} write=${record.cacheWrite} out=${record.output}`,\n )\n\n if (this.config.revertPing) await this.revertPing(sessionID, startAt)\n } catch (err) {\n this.logger.warn(`ping ${short(sessionID)} failed`, errText(err))\n } finally {\n s.warming = false\n s.lastPingAt = Date.now()\n // Schedule relative to the intended time so ping duration doesn't\n // compound drift. Floor at now+1s to avoid immediate re-fire if the\n // ping took longer than the interval.\n s.nextPingAt = Math.max(\n Date.now() + 1_000,\n s.nextPingAt + jitter(this.config.intervalMs),\n )\n this.store.persist()\n }\n }\n\n /** Remove the `~` user + `~` assistant turn so it never enters real context. */\n private async revertPing(sessionID: string, startAt: number): Promise<void> {\n let messages: any[]\n try {\n const res = await this.client.session.messages({ path: { id: sessionID } })\n messages = res?.data ?? res ?? []\n } catch (err) {\n this.logger.warn(`ping ${short(sessionID)} revert: list failed`, errText(err))\n return\n }\n\n let target: string | undefined\n for (const m of messages) {\n if (m?.info?.role !== \"user\") continue\n if (Number(m.info?.time?.created ?? 0) < startAt - 2_000) continue\n const text = (m.parts ?? [])\n .filter((p: any) => p?.type === \"text\")\n .map((p: any) => p?.text ?? \"\")\n .join(\"\")\n .trim()\n if (text === this.config.pingToken) target = m.info.id\n }\n\n if (!target) {\n this.logger.dbg(`ping ${short(sessionID)} revert: ping message not found`)\n return\n }\n\n try {\n await this.client.session.revert({\n path: { id: sessionID },\n body: { messageID: target },\n })\n } catch (err) {\n // BusyError (a real turn started) or similar — leave the ping visible.\n this.logger.warn(`ping ${short(sessionID)} revert failed`, errText(err))\n }\n }\n\n private async resolveSession(s: SessionKeepalive): Promise<void> {\n try {\n const res = await this.client.session.get({ path: { id: s.sessionID } })\n const info = res?.data ?? res\n if (info?.parentID && !this.config.includeChildSessions) {\n s.eligible = false\n s.modelLabel = info?.model?.id ?? \"child\"\n return\n }\n s.modelLabel = info?.model?.id ?? \"unknown\"\n s.eligible = isEligibleModel(this.config, info?.model?.providerID, info?.model?.id)\n } catch (err) {\n s.eligible = false\n this.logger.dbg(`resolve ${short(s.sessionID)} failed`, errText(err))\n }\n }\n\n private pollControl(): void {\n const control = readControl(this.directory)\n if (!control || control.updatedAt <= this.controlUpdatedAt) return\n this.controlUpdatedAt = control.updatedAt\n if (control.enabled === this.enabled) return\n\n this.enabled = control.enabled\n this.store.setEnabled(control.enabled)\n const now = Date.now()\n for (const session of this.store.all()) {\n if (!control.enabled) {\n session.active = false\n continue\n }\n // Only re-arm sessions that have had a real turn (cache to warm) and\n // are not currently occupied.\n if (!session.eligible || session.busy || session.warming) continue\n if (!session.lastResponseAt) continue\n session.idleSince = now\n session.windowEndsAt = now + this.config.windowMs\n session.nextPingAt = now + this.config.intervalMs\n session.pingsSent = 0\n session.lastPing = undefined\n session.active = true\n }\n this.store.persist()\n this.logger.info(`runtime ${control.enabled ? \"enabled\" : \"disabled\"}`)\n }\n\n /** Recover when a session.status idle event is missed by reconciling with the API. */\n private async reconcileStatus(): Promise<void> {\n if (this.reconcilingStatus) return\n const busy = this.store.all().filter((session) => session.busy && !session.warming)\n if (busy.length === 0) return\n\n this.reconcilingStatus = true\n try {\n const res = await this.client.session.status()\n const statuses = res?.data ?? res ?? {}\n for (const session of busy) {\n const status = statuses?.[session.sessionID]?.type ?? \"idle\"\n if (status === \"idle\") await this.armWindow(session.sessionID)\n }\n } catch (err) {\n this.logger.dbg(\"status reconciliation failed\", errText(err))\n } finally {\n this.reconcilingStatus = false\n }\n }\n}\n\nfunction short(sessionID: string): string {\n return sessionID.slice(-6)\n}\n\nfunction jitter(base: number): number {\n return Math.max(1_000, base + Math.floor((Math.random() * 2 - 1) * JITTER_MS))\n}\n\nfunction errText(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n","import { appendFileSync, mkdirSync } from \"node:fs\"\nimport { dirname } from \"node:path\"\n\nconst PREFIX = \"[cache-keepalive]\"\n\nexport class Logger {\n private readonly logPath: string\n private readonly debug: boolean\n private initialized = false\n\n constructor(debug: boolean, logPath: string) {\n this.debug = debug\n this.logPath = logPath\n }\n\n info(message: string, data?: unknown) {\n this.write(\"INFO\", message, data)\n }\n\n warn(message: string, data?: unknown) {\n this.write(\"WARN\", message, data)\n }\n\n error(message: string, data?: unknown) {\n this.write(\"ERROR\", message, data)\n }\n\n dbg(message: string, data?: unknown) {\n if (this.debug) this.write(\"DEBUG\", message, data)\n }\n\n private write(level: string, message: string, data?: unknown): void {\n const ts = new Date().toISOString()\n const extra =\n data !== undefined ? ` ${typeof data === \"string\" ? data : JSON.stringify(data)}` : \"\"\n const line = `${ts} ${level} ${PREFIX} ${message}${extra}\\n`\n try {\n if (!this.initialized) {\n mkdirSync(dirname(this.logPath), { recursive: true })\n this.initialized = true\n }\n appendFileSync(this.logPath, line, \"utf8\")\n } catch {\n // Best-effort logging — never crash the plugin.\n }\n }\n}\n","import { mkdir, rename, writeFile } from \"node:fs/promises\"\nimport { dirname } from \"node:path\"\nimport type { KeepaliveConfig } from \"./config\"\nimport { stateFilePath } from \"./paths\"\n\nexport type PingRecord = {\n at: number\n hit: boolean\n input: number\n cacheRead: number\n cacheWrite: number\n output: number\n}\n\n/** Live per-session bookkeeping held by the server plugin. */\nexport type SessionKeepalive = {\n sessionID: string\n /** Whether the session's provider/model pair supports cache warming. */\n eligible: boolean\n modelLabel?: string\n /** Epoch ms of the last real assistant response (excludes ping replies). */\n lastResponseAt: number\n /** Epoch ms the current idle stretch began. */\n idleSince: number\n /** Epoch ms after which warming stops and the cache is allowed to go cold. */\n windowEndsAt: number\n /** Epoch ms the next ping is due. */\n nextPingAt: number\n /** Number of scheduled ping requests attempted during this warm window. */\n pingsSent: number\n /** A real turn is currently running. */\n busy: boolean\n /** Epoch ms when `busy` was last set; used as a stuck-busy timeout backstop. */\n busySince?: number\n /** A keepalive ping is currently in flight. */\n warming: boolean\n /** Within the warm window and actively scheduling pings. */\n active: boolean\n /** True while `armWindow` is resolving session metadata (async guard). */\n arming?: boolean\n /** Epoch ms of the last ping; retained only to ignore its late events. */\n lastPingAt?: number\n lastPing?: PingRecord\n}\n\n/** Lean projection persisted to disk for the TUI reader. */\nexport type PersistedSession = {\n eligible: boolean\n modelLabel?: string\n lastResponseAt: number\n idleSince: number\n windowEndsAt: number\n nextPingAt: number\n intervalMs: number\n pingsSent: number\n busy: boolean\n warming: boolean\n active: boolean\n lastPing?: PingRecord\n}\n\nexport type PersistedState = {\n version: 1\n updatedAt: number\n enabled: boolean\n intervalMs: number\n windowMs: number\n sessions: Record<string, PersistedSession>\n}\n\nexport class KeepaliveStore {\n private readonly sessions = new Map<string, SessionKeepalive>()\n private writeQueue: Promise<void> = Promise.resolve()\n private enabled: boolean\n\n constructor(\n private readonly config: KeepaliveConfig,\n private readonly directory: string,\n ) {\n this.enabled = config.enabled\n }\n\n setEnabled(enabled: boolean): void {\n this.enabled = enabled\n }\n\n get(sessionID: string): SessionKeepalive | undefined {\n return this.sessions.get(sessionID)\n }\n\n ensure(sessionID: string): SessionKeepalive {\n const existing = this.sessions.get(sessionID)\n if (existing) return existing\n const created: SessionKeepalive = {\n sessionID,\n eligible: false,\n lastResponseAt: 0,\n idleSince: 0,\n windowEndsAt: 0,\n nextPingAt: 0,\n pingsSent: 0,\n busy: false,\n warming: false,\n active: false,\n }\n this.sessions.set(sessionID, created)\n return created\n }\n\n remove(sessionID: string): void {\n this.sessions.delete(sessionID)\n }\n\n all(): SessionKeepalive[] {\n return [...this.sessions.values()]\n }\n\n /** Atomically persist a lean snapshot for the TUI. Never throws to callers. */\n persist(): void {\n const snapshot: PersistedState = {\n version: 1,\n updatedAt: Date.now(),\n enabled: this.enabled,\n intervalMs: this.config.intervalMs,\n windowMs: this.config.windowMs,\n sessions: Object.fromEntries(\n this.all().map((s) => [\n s.sessionID,\n {\n eligible: s.eligible,\n modelLabel: s.modelLabel,\n lastResponseAt: s.lastResponseAt,\n idleSince: s.idleSince,\n windowEndsAt: s.windowEndsAt,\n nextPingAt: s.nextPingAt,\n intervalMs: this.config.intervalMs,\n pingsSent: s.pingsSent,\n busy: s.busy,\n warming: s.warming,\n active: s.active,\n lastPing: s.lastPing,\n } satisfies PersistedSession,\n ]),\n ),\n }\n\n const write = () => atomicWrite(stateFilePath(this.directory), JSON.stringify(snapshot))\n this.writeQueue = this.writeQueue.then(write, write)\n }\n}\n\nasync function atomicWrite(path: string, contents: string): Promise<void> {\n try {\n const tmp = `${path}.${process.pid}.${Date.now()}.tmp`\n await mkdir(dirname(path), { recursive: true })\n await writeFile(tmp, `${contents}\\n`, \"utf8\")\n await rename(tmp, path)\n } catch {\n // Persistence is best-effort; the TUI readout simply falls behind.\n }\n}\n","/**\n * Stable keepalive instruction appended to the system prompt.\n *\n * IMPORTANT: this text must be byte-stable and applied to *every* request for an\n * eligible model (real turns and pings alike). If it were only added on pings, the\n * system prefix would differ from real turns and the ping would miss the cache.\n * Adding it consistently keeps the cached prefix identical and lets the model\n * answer a ping with a single token instead of a paragraph or a tool call.\n */\nexport function keepaliveInstruction(pingToken: string): string {\n return [\n \"<cache_keepalive>\",\n `A user message consisting of exactly the single token \"${pingToken}\" is an`,\n \"automated cache-keepalive ping, not a real request. When you receive it,\",\n `reply with exactly \"${pingToken}\" and nothing else. Do not call any tools,`,\n \"do not explain, and do not take any action.\",\n \"</cache_keepalive>\",\n ].join(\"\\n\")\n}\n","import type { Plugin } from \"@opencode-ai/plugin\"\nimport { configureClientAuth, isSecureMode } from \"./lib/auth\"\nimport { getConfig } from \"./lib/config\"\nimport { KeepaliveEngine } from \"./lib/keepalive\"\nimport { Logger } from \"./lib/logger\"\nimport { isEligibleModel } from \"./lib/model\"\nimport { logFilePath } from \"./lib/paths\"\nimport { KeepaliveStore } from \"./lib/state\"\nimport { keepaliveInstruction } from \"./lib/system\"\n\ndeclare const __KEEPALIVE_VERSION__: string\n\nconst server: Plugin = async (ctx, options) => {\n const config = getConfig(options as Record<string, unknown> | undefined)\n const logger = new Logger(config.debug, logFilePath())\n\n if (!config.enabled) {\n logger.info(\"disabled via config\")\n return {}\n }\n\n if (isSecureMode()) configureClientAuth(ctx.client)\n\n const store = new KeepaliveStore(config, ctx.directory)\n const engine = new KeepaliveEngine(ctx.client, config, store, logger, ctx.directory)\n engine.start()\n\n const version = typeof __KEEPALIVE_VERSION__ !== \"undefined\" ? __KEEPALIVE_VERSION__ : \"dev\"\n logger.info(`v${version} ready`)\n\n const instruction = keepaliveInstruction(config.pingToken)\n\n return {\n event: async ({ event }) => {\n await engine.onEvent(event)\n },\n\n // Keep the system prefix identical between real turns and pings, and make\n // the model answer a ping with a single token.\n \"experimental.chat.system.transform\": async (input, output) => {\n if (!config.injectSystemInstruction) return\n const model = input.model as { providerID?: string; id?: string; modelID?: string }\n if (!isEligibleModel(config, model?.providerID, model?.id ?? model?.modelID)) return\n output.system.push(instruction)\n },\n\n // Backstop: if the model ignores the instruction and tries a tool during a\n // ping, block it — the ping turn is going to be reverted anyway.\n \"tool.execute.before\": async (input) => {\n if (engine.isWarming(input.sessionID)) {\n throw new Error(\"cache-keepalive: tools are disabled during a keepalive ping\")\n }\n },\n\n dispose: async () => {\n engine.stop()\n },\n }\n}\n\nexport default server\n"],"mappings":"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","names":["dirname","mkdir","rename","writeFile","dirname","mkdir","dirname","writeFile","rename"]}
|
|
1
|
+
{"version":3,"sources":["../lib/auth.ts","../lib/config.ts","../lib/control.ts","../lib/paths.ts","../lib/model.ts","../lib/keepalive.ts","../lib/logger.ts","../lib/state.ts","../lib/system.ts","../index.ts"],"sourcesContent":["/**\n * When opencode runs with `OPENCODE_SERVER_PASSWORD`, the local HTTP API requires\n * Basic auth. The SDK client handed to plugins does not carry it automatically,\n * so we attach an interceptor. No-op when the server is not password protected.\n *\n * Same approach as opencode-dynamic-context-pruning's auth helper.\n */\nexport function isSecureMode(): boolean {\n return !!process.env.OPENCODE_SERVER_PASSWORD\n}\n\nexport function getAuthorizationHeader(): string | undefined {\n const password = process.env.OPENCODE_SERVER_PASSWORD\n if (!password) return undefined\n const username = process.env.OPENCODE_SERVER_USERNAME ?? \"opencode\"\n const credentials = Buffer.from(`${username}:${password}`).toString(\"base64\")\n return `Basic ${credentials}`\n}\n\nexport function configureClientAuth(client: any): void {\n const header = getAuthorizationHeader()\n if (!header) return\n\n const inner = client?._client ?? client?.client\n if (!inner?.interceptors?.request) return\n\n inner.interceptors.request.use((request: Request) => {\n if (!request.headers.has(\"Authorization\")) {\n request.headers.set(\"Authorization\", header)\n }\n return request\n })\n}\n","export type KeepaliveConfig = {\n /** Master switch. When false the plugin registers no timers or hooks. */\n enabled: boolean\n /** Milliseconds between pings. Must stay under the shortest supported provider TTL. */\n intervalMs: number\n /**\n * How long to keep warming after the last real response before giving up and\n * letting the cache go cold. The default follows the Copilot Claude\n * write/read break-even; providers with automatic caching may be cheaper.\n */\n windowMs: number\n /**\n * Append a stable keepalive instruction to the system prompt so the model\n * answers a `~` ping with a single `~` token and never calls tools. The\n * instruction is added to real turns too, so the cached prefix stays identical.\n */\n injectSystemInstruction: boolean\n /** The single character/token used as the keepalive suffix. */\n pingToken: string\n /** Remove synthetic ping turns after measuring their cache usage. */\n revertPing: boolean\n /** Warm subagent/child sessions too. Off by default to avoid wasted pings. */\n includeChildSessions: boolean\n debug: boolean\n /** providerID substrings eligible for warming (cache-controlled providers). */\n providerAllowlist: string[]\n /** modelID substrings eligible for warming. */\n modelAllowlist: string[]\n}\n\nconst DEFAULTS: KeepaliveConfig = {\n enabled: true,\n intervalMs: 270_000, // 4.5 min — below the shortest supported cache TTL\n windowMs: 3_300_000, // 55 min — the Copilot Claude write/read break-even\n injectSystemInstruction: true,\n pingToken: \"~\",\n revertPing: true,\n includeChildSessions: false,\n debug: false,\n providerAllowlist: [\"copilot\"],\n modelAllowlist: [\"claude\", \"anthropic\", \"sonnet\", \"opus\", \"haiku\", \"gpt\"],\n}\n\nexport function getConfig(options: Record<string, unknown> | undefined): KeepaliveConfig {\n const o = options ?? {}\n const env = process.env\n\n return {\n enabled: boolOpt(o.enabled, env.OPENCODE_KEEPALIVE_ENABLED !== \"false\" && DEFAULTS.enabled),\n intervalMs: durationOpt(\n o.intervalMs,\n o.intervalSeconds,\n numEnv(env.OPENCODE_KEEPALIVE_INTERVAL_MS, DEFAULTS.intervalMs),\n ),\n windowMs: durationOpt(\n o.windowMs,\n o.windowMinutes ? Number(o.windowMinutes) * 60 : undefined,\n numEnv(env.OPENCODE_KEEPALIVE_WINDOW_MS, DEFAULTS.windowMs),\n ),\n injectSystemInstruction: boolOpt(\n o.injectSystemInstruction,\n DEFAULTS.injectSystemInstruction,\n ),\n pingToken:\n typeof o.pingToken === \"string\" && o.pingToken.length > 0\n ? o.pingToken\n : DEFAULTS.pingToken,\n revertPing: boolOpt(\n o.revertPing,\n env.OPENCODE_KEEPALIVE_REVERT_PING !== \"false\" && DEFAULTS.revertPing,\n ),\n includeChildSessions: boolOpt(o.includeChildSessions, DEFAULTS.includeChildSessions),\n debug: boolOpt(o.debug, env.OPENCODE_KEEPALIVE_DEBUG === \"true\"),\n providerAllowlist: listOpt(o.providerAllowlist, DEFAULTS.providerAllowlist),\n modelAllowlist: listOpt(o.modelAllowlist, DEFAULTS.modelAllowlist),\n }\n}\n\nfunction boolOpt(value: unknown, fallback: boolean): boolean {\n return typeof value === \"boolean\" ? value : fallback\n}\n\nfunction listOpt(value: unknown, fallback: string[]): string[] {\n if (!Array.isArray(value)) return fallback\n const filtered = value.filter((item): item is string => typeof item === \"string\")\n return filtered.length > 0 ? filtered : fallback\n}\n\n/** Accept an explicit `*Ms` option, a `*Seconds` option, or fall back. */\nfunction durationOpt(ms: unknown, seconds: unknown, fallback: number): number {\n if (typeof ms === \"number\" && Number.isFinite(ms) && ms > 0) return ms\n if (typeof seconds === \"number\" && Number.isFinite(seconds) && seconds > 0)\n return seconds * 1000\n return fallback\n}\n\nfunction numEnv(raw: string | undefined, fallback: number): number {\n if (!raw) return fallback\n const value = Number(raw)\n return Number.isFinite(value) && value > 0 ? value : fallback\n}\n","import { readFileSync } from \"node:fs\"\nimport { mkdir, rename, writeFile } from \"node:fs/promises\"\nimport { dirname } from \"node:path\"\nimport { controlFilePath } from \"./paths\"\n\nexport type KeepaliveControl = {\n version: 1\n enabled: boolean\n updatedAt: number\n}\n\nexport function readControl(directory: string): KeepaliveControl | undefined {\n try {\n const value = JSON.parse(\n readFileSync(controlFilePath(directory), \"utf8\"),\n ) as Partial<KeepaliveControl>\n if (value.version !== 1 || typeof value.enabled !== \"boolean\") return undefined\n if (typeof value.updatedAt !== \"number\") return undefined\n return value as KeepaliveControl\n } catch {\n return undefined\n }\n}\n\nexport async function writeControl(enabled: boolean, directory: string): Promise<KeepaliveControl> {\n const value: KeepaliveControl = {\n version: 1,\n enabled,\n updatedAt: Date.now(),\n }\n const path = controlFilePath(directory)\n const tmp = `${path}.${process.pid}.${value.updatedAt}.tmp`\n await mkdir(dirname(path), { recursive: true })\n await writeFile(tmp, `${JSON.stringify(value)}\\n`, \"utf8\")\n await rename(tmp, path)\n return value\n}\n","import { createHash } from \"node:crypto\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\n\n/**\n * Absolute path to the JSON state file shared between the server plugin (writer)\n * and the TUI plugin (reader). Both processes run on the same machine, so a file\n * under opencode's plugin storage dir is the simplest cross-process channel.\n *\n * Mirrors the storage layout used by other opencode plugins:\n * <XDG_DATA_HOME | ~/.local/share>/opencode/storage/plugin/keepalive/state.json\n */\nexport function stateFilePath(directory: string): string {\n const key = createHash(\"sha256\").update(directory).digest(\"hex\").slice(0, 16)\n return join(stateDirectoryPath(), `state-${key}.json`)\n}\n\nexport function stateDirectoryPath(): string {\n return join(\n process.env.XDG_DATA_HOME ?? join(homedir(), \".local\", \"share\"),\n \"opencode\",\n \"storage\",\n \"plugin\",\n \"keepalive\",\n )\n}\n\n/**\n * Runtime on/off control written by the TUI and read by the server plugin.\n * Scoped per directory (like the state file) so toggling keepalive in one\n * project does not affect every other open opencode session.\n */\nexport function controlFilePath(directory: string): string {\n const key = createHash(\"sha256\").update(directory).digest(\"hex\").slice(0, 16)\n return join(stateDirectoryPath(), `control-${key}.json`)\n}\n\n/** Append-only log file for the server plugin — never stdout, which corrupts the TUI. */\nexport function logFilePath(): string {\n return join(stateDirectoryPath(), \"server.log\")\n}\n","import type { KeepaliveConfig } from \"./config\"\n\n/**\n * Warming only helps on providers that do prefix caching we can refresh by\n * replaying the prefix. Supported defaults include Claude and GPT models served\n * through GitHub Copilot. Everything else is skipped.\n */\nexport function isEligibleModel(\n config: KeepaliveConfig,\n providerID: string | undefined,\n modelID: string | undefined,\n): boolean {\n if (!providerID || !modelID) return false\n const provider = providerID.toLowerCase()\n const model = modelID.toLowerCase()\n const providerOk = config.providerAllowlist.some((needle) => provider.includes(needle))\n const modelOk = config.modelAllowlist.some((needle) => model.includes(needle))\n return providerOk && modelOk\n}\n","import type { KeepaliveConfig } from \"./config\"\nimport { readControl } from \"./control\"\nimport { isEligibleModel } from \"./model\"\nimport type { InflightPing, KeepaliveStore, PingRequest, SessionKeepalive } from \"./state\"\nimport type { Logger } from \"./logger\"\n\nconst TICK_MS = 15_000\nconst JITTER_MS = 15_000\nconst CONTROL_POLL_MS = 1_000\nconst STATUS_POLL_MS = 2_000\nconst PING_STATUS_GRACE_MS = 5_000\n/**\n * A cache last touched more than this many intervals ago is presumed cold. Pinging\n * it would pay a full cache write for a session nobody is using, so the window\n * stays closed until the next real turn re-arms it.\n */\nconst COLD_AFTER_INTERVALS = 2\n/** Recent messages scanned for ping reverts and request-setting recovery. */\nconst MESSAGE_LOOKUP_LIMIT = 50\n\n/**\n * The keepalive engine.\n *\n * Lifecycle per session:\n * 1. A real turn finishes -> `session.status` idle -> arm a warm window anchored on\n * the real response and schedule pings.\n * 2. On each tick past `nextPingAt` (and within the window) -> `firePing`.\n * 3. `firePing` confirms with the server that the session is idle, sends a `~`\n * prompt with the last real turn's agent/model/variant (exact cached prefix),\n * reads `usage` to confirm a cache hit, then optionally reverts the `~`/`~` turn.\n * 4. A new real user turn, the window closing, or the cache presumed cold stops warming.\n *\n * Threading: all state is mutated on the single plugin event loop. opencode invokes\n * the `event` hook without awaiting it, so async handlers interleave; per-session\n * `arming` / `warming` flags serialize the async sections.\n *\n * Errors: network/API failures are logged and never thrown to opencode.\n */\nexport class KeepaliveEngine {\n private timer: ReturnType<typeof setInterval> | undefined\n private controlTimer: ReturnType<typeof setInterval> | undefined\n private statusTimer: ReturnType<typeof setInterval> | undefined\n private controlUpdatedAt = 0\n private reconcilingStatus = false\n private enabled: boolean\n\n constructor(\n private readonly client: any,\n private readonly config: KeepaliveConfig,\n private readonly store: KeepaliveStore,\n private readonly logger: Logger,\n private readonly directory: string,\n ) {\n const control = readControl(directory)\n this.enabled = control?.enabled ?? config.enabled\n this.controlUpdatedAt = control?.updatedAt ?? 0\n this.store.setEnabled(this.enabled)\n }\n\n start(): void {\n if (this.timer) return\n this.timer = setInterval(() => void this.tick(), TICK_MS)\n this.controlTimer = setInterval(() => this.pollControl(), CONTROL_POLL_MS)\n this.statusTimer = setInterval(() => void this.reconcileStatus(), STATUS_POLL_MS)\n this.store.persist()\n this.logger.info(\n `started (${this.enabled ? \"enabled\" : \"disabled\"}, ` +\n `interval=${Math.round(this.config.intervalMs / 1000)}s, ` +\n `window=${Math.round(this.config.windowMs / 60_000)}m)`,\n )\n }\n\n stop(): void {\n if (!this.timer) return\n clearInterval(this.timer)\n this.timer = undefined\n if (this.controlTimer) clearInterval(this.controlTimer)\n this.controlTimer = undefined\n if (this.statusTimer) clearInterval(this.statusTimer)\n this.statusTimer = undefined\n }\n\n /**\n * True while a ping turn owns the session, so tool calls must be blocked. Becomes\n * false as soon as a real user message joins the in-flight ping run: the rest of\n * that run is real work and needs its tools.\n */\n shouldBlockTools(sessionID: string): boolean {\n const s = this.store.get(sessionID)\n return !!s?.warming && !s.ping?.interrupted\n }\n\n async onEvent(event: any): Promise<void> {\n const type: string | undefined = event?.type\n if (!type) return\n\n if (type === \"session.deleted\") {\n const id = event.properties?.sessionID ?? event.properties?.info?.id\n if (id) {\n this.store.remove(id)\n this.store.persist()\n }\n return\n }\n\n if (type === \"message.part.updated\") {\n const part = event.properties?.part\n const sessionID: string | undefined = part?.sessionID ?? event.properties?.sessionID\n if (!sessionID) return\n const session = this.store.get(sessionID)\n if (session?.warming) this.notePingRunPart(session, part)\n return\n }\n\n if (type === \"message.updated\") {\n const info = event.properties?.info\n const sessionID: string | undefined = info?.sessionID ?? event.properties?.sessionID\n if (!sessionID) return\n const session = this.store.get(sessionID)\n // Everything inside our own ping turn is ignored — except a real user\n // message, which opencode joins to the running ping loop.\n if (session?.warming) {\n if (info?.role === \"user\") this.notePingRunUser(session, info)\n return\n }\n const eventAt = info?.time?.completed ?? info?.time?.created\n // Events are asynchronous: a completed ping may still emit message updates\n // after `warming` clears. Their timestamps predate the recorded ping.\n if (session?.lastPingAt && typeof eventAt === \"number\" && eventAt <= session.lastPingAt)\n return\n\n if (info?.role === \"user\") {\n const s = this.store.ensure(sessionID)\n // opencode re-emits older user messages (e.g. summary updates after a\n // turn); only a newly created message starts a turn.\n if (!isNewUserMessage(s, info)) return\n noteUserMessage(s, info)\n s.busy = true\n s.active = false\n this.store.persist()\n return\n }\n\n if (info?.role === \"assistant\" && typeof info?.time?.completed === \"number\") {\n const s = this.store.ensure(sessionID)\n s.lastResponseAt = info.time.completed\n this.store.persist()\n }\n return\n }\n\n if (type === \"session.status\") {\n const sessionID: string | undefined = event.properties?.sessionID\n const status: string | undefined = event.properties?.status?.type\n if (!sessionID || !status) return\n\n // The ping and its optional revert emit status transitions after their\n // requests resolve. Ignore them briefly so they cannot re-arm/reset the\n // active warm window. Real user-message events still stop warming now.\n const current = this.store.get(sessionID)\n if (\n current?.warming ||\n (status !== \"idle\" &&\n current?.lastPingAt &&\n Date.now() - current.lastPingAt < PING_STATUS_GRACE_MS)\n )\n return\n\n if (status === \"idle\") {\n await this.armWindow(sessionID)\n return\n }\n\n const s = this.store.ensure(sessionID)\n s.busy = true\n s.active = false\n this.store.persist()\n return\n }\n\n // Compatibility fallback for opencode versions predating session.status.\n if (type === \"session.idle\") {\n const sessionID: string | undefined = event.properties?.sessionID\n if (!sessionID) return\n if (this.store.get(sessionID)?.warming) return // idle from our own ping\n await this.armWindow(sessionID)\n }\n }\n\n /**\n * Track a new user message observed while a ping is in flight. Event order alone\n * cannot tell the ping's own message from a real prompt racing it, so messages are\n * held until their text part arrives (see `notePingRunPart`). Any message seen\n * before the ping is sent, or a second new message, is necessarily a real turn.\n */\n private notePingRunUser(s: SessionKeepalive, info: any): void {\n const ping = s.ping\n if (!ping || typeof info?.id !== \"string\") return\n const created = info?.time?.created\n if (typeof created === \"number\" && created < ping.startedAt) return // re-emitted old message\n if (info.id === ping.messageID || ping.pending.has(info.id)) return\n if (!ping.sent || ping.messageID) {\n this.interruptPing(s, info)\n return\n }\n ping.pending.set(info.id, info)\n if (ping.pending.size > 1) this.interruptPing(s)\n }\n\n /**\n * Classify a pending user message by its first text part: the ping token marks\n * the ping's own message; any other text is a real turn joining the ping run.\n * opencode publishes a user message's parts right after the message itself, before\n * the run reaches any tool call.\n */\n private notePingRunPart(s: SessionKeepalive, part: any): void {\n const ping = s.ping\n if (!ping || part?.type !== \"text\" || part?.synthetic) return\n const id = part?.messageID\n if (typeof id !== \"string\" || !ping.pending.has(id)) return\n const info = ping.pending.get(id)\n ping.pending.delete(id)\n if (!ping.messageID && String(part.text ?? \"\").trim() === this.config.pingToken) {\n ping.messageID = id\n return\n }\n this.interruptPing(s, info)\n }\n\n /** A real turn joined the in-flight ping: unblock tools and treat the session as busy. */\n private interruptPing(s: SessionKeepalive, info?: unknown): void {\n const ping = s.ping\n if (!ping) return\n if (!ping.interrupted) this.logger.dbg(`real turn joined ping ${short(s.sessionID)}`)\n ping.interrupted = true\n if (info) noteUserMessage(s, info)\n s.busy = true\n s.active = false\n this.store.persist()\n }\n\n private async armWindow(sessionID: string): Promise<void> {\n const s = this.store.ensure(sessionID)\n // Duplicate idle events (including late events from a ping) must not reset the\n // current idle stretch or extend its warm window.\n if (s.active && !s.busy) return\n // Serialize: only one armWindow may resolve metadata at a time per session.\n // This prevents the session.status + session.idle double-event race from\n // producing two concurrent resolveSession calls that can clobber each other.\n if (s.arming) return\n s.arming = true\n try {\n s.busy = false\n await this.resolveSession(s)\n\n // A new turn may have started while session metadata was being resolved.\n if (s.busy || !this.enabled || !s.eligible) {\n s.active = false\n this.store.persist()\n return\n }\n\n // Only a recent real response leaves a cache worth warming. Idle events can\n // also fire for sessions untouched for hours (restarts, re-emitted messages).\n const now = Date.now()\n if (!s.lastResponseAt || !this.cacheMayBeWarm(s, now)) {\n s.active = false\n this.store.persist()\n this.logger.dbg(`not armed ${short(sessionID)} — no recent real response`)\n return\n }\n\n this.openWindow(s, now)\n this.store.persist()\n this.logger.dbg(`armed ${short(sessionID)} model=${s.modelLabel}`)\n } finally {\n s.arming = false\n }\n }\n\n /** Start a warm window anchored on the last real response and the last cache touch. */\n private openWindow(s: SessionKeepalive, now: number): void {\n s.idleSince = now\n s.windowEndsAt = s.lastResponseAt + this.config.windowMs\n s.nextPingAt = Math.max(now, lastCacheTouch(s) + this.config.intervalMs)\n s.pingsSent = 0\n s.lastPing = undefined\n s.active = true\n }\n\n /** False once the cache has gone too long without a request to still be trusted warm. */\n private cacheMayBeWarm(s: SessionKeepalive, now: number): boolean {\n const touch = lastCacheTouch(s)\n return touch > 0 && now - touch <= COLD_AFTER_INTERVALS * this.config.intervalMs\n }\n\n private async tick(): Promise<void> {\n if (!this.enabled) return\n const now = Date.now()\n for (const s of this.store.all()) {\n if (!s.eligible || !s.active || s.busy || s.warming) continue\n\n if (now >= s.windowEndsAt) {\n s.active = false\n this.store.persist()\n this.logger.dbg(\n `window closed ${short(s.sessionID)} after ${s.pingsSent} ping(s) — cache may cool`,\n )\n continue\n }\n\n if (now < s.nextPingAt) continue\n\n // Far overdue (host sleep, stalled event loop): the cache is presumed cold.\n // Stop rather than pay a cold write or fire a catch-up burst.\n if (!this.cacheMayBeWarm(s, now)) {\n s.active = false\n this.store.persist()\n this.logger.dbg(\n `window closed ${short(s.sessionID)} — ping overdue, cache presumed cold`,\n )\n continue\n }\n\n void this.firePing(s.sessionID)\n }\n }\n\n private async firePing(sessionID: string): Promise<void> {\n const s = this.store.get(sessionID)\n if (!s || s.warming || s.busy || !s.active) return\n\n const ping: InflightPing = {\n startedAt: Date.now(),\n sent: false,\n pending: new Map(),\n interrupted: false,\n }\n s.warming = true\n s.ping = ping\n this.store.persist()\n\n try {\n // opencode joins a prompt to a running turn instead of rejecting it, so a\n // missed busy signal would inject the ping into real work. Ask the server.\n if (!(await this.isIdleOnServer(sessionID))) {\n s.busy = true\n s.active = false\n this.logger.dbg(`ping ${short(sessionID)} skipped — session busy`)\n return\n }\n\n const request = s.request ?? (await this.lookupRequest(s))\n if (!request) {\n s.active = false\n this.logger.dbg(`ping ${short(sessionID)} skipped — last real request unknown`)\n return\n }\n\n // A real turn may have started (or keepalive been disabled) while checking.\n if (ping.interrupted || s.busy || !s.active || !this.enabled) return\n\n ping.sent = true\n // Count every request attempt. A transport/provider failure still consumed a\n // scheduled ping slot and must remain visible in the footer.\n s.pingsSent += 1\n this.store.persist()\n\n const res = await this.client.session.prompt({\n path: { id: sessionID },\n body: {\n parts: [{ type: \"text\", text: this.config.pingToken }],\n ...(request.agent ? { agent: request.agent } : {}),\n model: { providerID: request.providerID, modelID: request.modelID },\n ...(request.variant ? { variant: request.variant } : {}),\n },\n })\n const info = res?.data?.info ?? res?.info\n\n if (ping.interrupted) {\n // A real turn ran inside this request: its reply is real work. Keep it,\n // do not score it as a ping, and re-arm from its completion.\n const completed = info?.time?.completed\n if (typeof completed === \"number\") s.lastResponseAt = completed\n this.logger.dbg(\n `ping ${short(sessionID)} absorbed a real turn — kept, not reverted`,\n )\n return\n }\n\n const cache = info?.tokens?.cache ?? {}\n const record = {\n at: Date.now(),\n hit: Number(cache.read ?? 0) > 0,\n input: Number(info?.tokens?.input ?? 0),\n cacheRead: Number(cache.read ?? 0),\n cacheWrite: Number(cache.write ?? 0),\n output: Number(info?.tokens?.output ?? 0),\n }\n s.lastPing = record\n this.logger.dbg(\n `ping ${short(sessionID)} ${record.hit ? \"HIT\" : \"MISS\"} ` +\n `in=${record.input} read=${record.cacheRead} write=${record.cacheWrite} out=${record.output}`,\n )\n\n if (this.config.revertPing) await this.revertPing(sessionID, ping)\n } catch (err) {\n this.logger.warn(`ping ${short(sessionID)} failed`, errText(err))\n } finally {\n s.warming = false\n s.ping = undefined\n const now = Date.now()\n if (ping.sent) {\n s.lastPingAt = now\n // Schedule relative to the intended time so ping duration doesn't\n // compound drift, but never into the past: that would fire catch-up\n // pings on consecutive ticks.\n const next = s.nextPingAt + jitter(this.config.intervalMs)\n s.nextPingAt = next > now ? next : now + jitter(this.config.intervalMs)\n }\n this.store.persist()\n if (ping.sent && ping.interrupted) {\n // The joined real turn has finished with this request; start its window.\n s.busy = false\n s.active = false\n void this.armWindow(sessionID)\n }\n }\n }\n\n /** True only when the server positively reports the session idle. */\n private async isIdleOnServer(sessionID: string): Promise<boolean> {\n try {\n const res = await this.client.session.status()\n const statuses = res?.data ?? res ?? {}\n return (statuses?.[sessionID]?.type ?? \"idle\") === \"idle\"\n } catch (err) {\n this.logger.dbg(`status check ${short(sessionID)} failed`, errText(err))\n return false\n }\n }\n\n /** Recover the last real turn's request settings when no user event was observed. */\n private async lookupRequest(s: SessionKeepalive): Promise<PingRequest | undefined> {\n const messages = await this.recentMessages(s.sessionID)\n if (!messages) return undefined\n for (let i = messages.length - 1; i >= 0; i--) {\n const m = messages[i]\n if (m?.info?.role !== \"user\" || this.isPingMessage(m)) continue\n const request = requestFrom(m.info)\n if (request) {\n s.request = request\n return request\n }\n }\n return undefined\n }\n\n private async recentMessages(sessionID: string): Promise<any[] | undefined> {\n try {\n const res = await this.client.session.messages({\n path: { id: sessionID },\n query: { limit: MESSAGE_LOOKUP_LIMIT },\n })\n const messages = res?.data ?? res\n return Array.isArray(messages) ? messages : undefined\n } catch (err) {\n this.logger.warn(`messages ${short(sessionID)} list failed`, errText(err))\n return undefined\n }\n }\n\n private isPingMessage(m: any): boolean {\n const text = (m?.parts ?? [])\n .filter((p: any) => p?.type === \"text\")\n .map((p: any) => p?.text ?? \"\")\n .join(\"\")\n .trim()\n return text === this.config.pingToken\n }\n\n /**\n * Remove the `~` user + `~` assistant turn so it never enters real context.\n * Revert drops the target and everything after it (and rolls back file\n * snapshots), so it is skipped unless only the ping's own replies follow.\n */\n private async revertPing(sessionID: string, ping: InflightPing): Promise<void> {\n const messages = await this.recentMessages(sessionID)\n if (!messages) return\n\n let index = -1\n for (let i = 0; i < messages.length; i++) {\n const m = messages[i]\n if (m?.info?.role !== \"user\") continue\n if (ping.messageID ? m.info.id === ping.messageID : this.isRecentPing(m, ping))\n index = i\n }\n if (index < 0) {\n this.logger.dbg(`ping ${short(sessionID)} revert: ping message not found`)\n return\n }\n\n const target: string = messages[index].info.id\n const foreign = messages\n .slice(index + 1)\n .some((m: any) => m?.info?.role !== \"assistant\" || m?.info?.parentID !== target)\n if (foreign) {\n this.logger.warn(`ping ${short(sessionID)} not reverted — other messages followed it`)\n return\n }\n\n try {\n await this.client.session.revert({\n path: { id: sessionID },\n body: { messageID: target },\n })\n } catch (err) {\n // BusyError (a real turn started) or similar — leave the ping visible.\n this.logger.warn(`ping ${short(sessionID)} revert failed`, errText(err))\n }\n }\n\n private isRecentPing(m: any, ping: InflightPing): boolean {\n return (\n Number(m?.info?.time?.created ?? 0) >= ping.startedAt - 2_000 && this.isPingMessage(m)\n )\n }\n\n private async resolveSession(s: SessionKeepalive): Promise<void> {\n try {\n const res = await this.client.session.get({ path: { id: s.sessionID } })\n const info = res?.data ?? res\n if (info?.parentID && !this.config.includeChildSessions) {\n s.eligible = false\n s.modelLabel = info?.model?.id ?? \"child\"\n return\n }\n s.modelLabel = info?.model?.id ?? \"unknown\"\n s.eligible = isEligibleModel(this.config, info?.model?.providerID, info?.model?.id)\n } catch (err) {\n s.eligible = false\n this.logger.dbg(`resolve ${short(s.sessionID)} failed`, errText(err))\n }\n }\n\n private pollControl(): void {\n const control = readControl(this.directory)\n if (!control || control.updatedAt <= this.controlUpdatedAt) return\n this.controlUpdatedAt = control.updatedAt\n if (control.enabled === this.enabled) return\n\n this.enabled = control.enabled\n this.store.setEnabled(control.enabled)\n const now = Date.now()\n for (const session of this.store.all()) {\n if (!control.enabled) {\n session.active = false\n continue\n }\n // Only re-arm sessions with a real response whose cache may still be warm,\n // and that are not currently occupied.\n if (!session.eligible || session.busy || session.warming) continue\n if (!session.lastResponseAt || !this.cacheMayBeWarm(session, now)) continue\n this.openWindow(session, now)\n }\n this.store.persist()\n this.logger.info(`runtime ${control.enabled ? \"enabled\" : \"disabled\"}`)\n }\n\n /** Recover when a session.status idle event is missed by reconciling with the API. */\n private async reconcileStatus(): Promise<void> {\n if (this.reconcilingStatus) return\n const busy = this.store.all().filter((session) => session.busy && !session.warming)\n if (busy.length === 0) return\n\n this.reconcilingStatus = true\n try {\n const res = await this.client.session.status()\n const statuses = res?.data ?? res ?? {}\n for (const session of busy) {\n const status = statuses?.[session.sessionID]?.type ?? \"idle\"\n if (status === \"idle\") await this.armWindow(session.sessionID)\n }\n } catch (err) {\n this.logger.dbg(\"status reconciliation failed\", errText(err))\n } finally {\n this.reconcilingStatus = false\n }\n }\n}\n\n/** Last time the session's prompt cache was known to be used (real response or ping). */\nfunction lastCacheTouch(s: SessionKeepalive): number {\n return Math.max(s.lastResponseAt, s.lastPingAt ?? 0)\n}\n\nfunction isNewUserMessage(s: SessionKeepalive, info: any): boolean {\n if (typeof info?.id === \"string\" && info.id === s.lastUserID) return false\n const created = info?.time?.created\n return typeof created !== \"number\" || created >= (s.lastUserAt ?? 0)\n}\n\nfunction noteUserMessage(s: SessionKeepalive, info: any): void {\n if (typeof info?.id === \"string\") s.lastUserID = info.id\n const created = info?.time?.created\n if (typeof created === \"number\") s.lastUserAt = created\n const request = requestFrom(info)\n if (request) s.request = request\n}\n\n/** Extract replayable request settings from a user message's info. */\nfunction requestFrom(info: any): PingRequest | undefined {\n const model = info?.model\n const providerID = model?.providerID\n const modelID = model?.modelID ?? model?.id\n if (typeof providerID !== \"string\" || typeof modelID !== \"string\") return undefined\n const variant = model?.variant ?? info?.variant\n return {\n agent: typeof info?.agent === \"string\" ? info.agent : undefined,\n providerID,\n modelID,\n variant: typeof variant === \"string\" && variant !== \"default\" ? variant : undefined,\n }\n}\n\nfunction short(sessionID: string): string {\n return sessionID.slice(-6)\n}\n\nfunction jitter(base: number): number {\n return Math.max(1_000, base + Math.floor((Math.random() * 2 - 1) * JITTER_MS))\n}\n\nfunction errText(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n","import { appendFileSync, mkdirSync } from \"node:fs\"\nimport { dirname } from \"node:path\"\n\nconst PREFIX = \"[cache-keepalive]\"\n\nexport class Logger {\n private readonly logPath: string\n private readonly debug: boolean\n private initialized = false\n\n constructor(debug: boolean, logPath: string) {\n this.debug = debug\n this.logPath = logPath\n }\n\n info(message: string, data?: unknown) {\n this.write(\"INFO\", message, data)\n }\n\n warn(message: string, data?: unknown) {\n this.write(\"WARN\", message, data)\n }\n\n error(message: string, data?: unknown) {\n this.write(\"ERROR\", message, data)\n }\n\n dbg(message: string, data?: unknown) {\n if (this.debug) this.write(\"DEBUG\", message, data)\n }\n\n private write(level: string, message: string, data?: unknown): void {\n const ts = new Date().toISOString()\n const extra =\n data !== undefined ? ` ${typeof data === \"string\" ? data : JSON.stringify(data)}` : \"\"\n const line = `${ts} ${level} ${PREFIX} ${message}${extra}\\n`\n try {\n if (!this.initialized) {\n mkdirSync(dirname(this.logPath), { recursive: true })\n this.initialized = true\n }\n appendFileSync(this.logPath, line, \"utf8\")\n } catch {\n // Best-effort logging — never crash the plugin.\n }\n }\n}\n","import { mkdir, rename, writeFile } from \"node:fs/promises\"\nimport { dirname } from \"node:path\"\nimport type { KeepaliveConfig } from \"./config\"\nimport { stateFilePath } from \"./paths\"\n\nexport type PingRecord = {\n at: number\n hit: boolean\n input: number\n cacheRead: number\n cacheWrite: number\n output: number\n}\n\n/**\n * Request settings of the last real user turn. A ping must replay them exactly:\n * a prompt without explicit settings runs as opencode's default agent with no\n * variant, which changes the request shape (missing the cache) and overwrites\n * the session's persisted agent/model/variant.\n */\nexport type PingRequest = {\n agent?: string\n providerID: string\n modelID: string\n variant?: string\n}\n\n/** Bookkeeping for the ping request currently in flight. */\nexport type InflightPing = {\n /** Epoch ms the ping attempt began (before the pre-flight status check). */\n startedAt: number\n /** The prompt request has been issued. */\n sent: boolean\n /** The ping's own user message, identified by its ping-token text part. */\n messageID?: string\n /**\n * New user messages seen during the ping whose text has not yet been observed.\n * Each is classified by its first text part: the ping token marks our own\n * message, anything else a real turn.\n */\n pending: Map<string, unknown>\n /**\n * A real user message appeared while the ping was in flight. opencode joins such\n * a prompt to the running ping loop, so the rest of the run is real work.\n */\n interrupted: boolean\n}\n\n/** Live per-session bookkeeping held by the server plugin. */\nexport type SessionKeepalive = {\n sessionID: string\n /** Whether the session's provider/model pair supports cache warming. */\n eligible: boolean\n modelLabel?: string\n /** Epoch ms of the last real assistant response (excludes ping replies). */\n lastResponseAt: number\n /** Epoch ms the current idle stretch began. */\n idleSince: number\n /** Epoch ms after which warming stops and the cache is allowed to go cold. */\n windowEndsAt: number\n /** Epoch ms the next ping is due. */\n nextPingAt: number\n /** Number of scheduled ping requests attempted during this warm window. */\n pingsSent: number\n /** A real turn is currently running. */\n busy: boolean\n /** A keepalive ping is currently in flight. */\n warming: boolean\n /** The in-flight ping; set exactly while `warming` is true. */\n ping?: InflightPing\n /** ID and creation time of the newest user message seen, to tell new turns from re-emitted old messages. */\n lastUserID?: string\n lastUserAt?: number\n /** Settings of the last real user turn, replayed by pings. */\n request?: PingRequest\n /** Within the warm window and actively scheduling pings. */\n active: boolean\n /** True while `armWindow` is resolving session metadata (async guard). */\n arming?: boolean\n /** Epoch ms the last sent ping finished; ignores its late events and marks the last cache touch. */\n lastPingAt?: number\n lastPing?: PingRecord\n}\n\n/** Lean projection persisted to disk for the TUI reader. */\nexport type PersistedSession = {\n eligible: boolean\n modelLabel?: string\n lastResponseAt: number\n idleSince: number\n windowEndsAt: number\n nextPingAt: number\n intervalMs: number\n pingsSent: number\n busy: boolean\n warming: boolean\n active: boolean\n lastPing?: PingRecord\n}\n\nexport type PersistedState = {\n version: 1\n updatedAt: number\n enabled: boolean\n intervalMs: number\n windowMs: number\n sessions: Record<string, PersistedSession>\n}\n\nexport class KeepaliveStore {\n private readonly sessions = new Map<string, SessionKeepalive>()\n private writeQueue: Promise<void> = Promise.resolve()\n private enabled: boolean\n\n constructor(\n private readonly config: KeepaliveConfig,\n private readonly directory: string,\n ) {\n this.enabled = config.enabled\n }\n\n setEnabled(enabled: boolean): void {\n this.enabled = enabled\n }\n\n get(sessionID: string): SessionKeepalive | undefined {\n return this.sessions.get(sessionID)\n }\n\n ensure(sessionID: string): SessionKeepalive {\n const existing = this.sessions.get(sessionID)\n if (existing) return existing\n const created: SessionKeepalive = {\n sessionID,\n eligible: false,\n lastResponseAt: 0,\n idleSince: 0,\n windowEndsAt: 0,\n nextPingAt: 0,\n pingsSent: 0,\n busy: false,\n warming: false,\n active: false,\n }\n this.sessions.set(sessionID, created)\n return created\n }\n\n remove(sessionID: string): void {\n this.sessions.delete(sessionID)\n }\n\n all(): SessionKeepalive[] {\n return [...this.sessions.values()]\n }\n\n /**\n * Atomically persist a lean snapshot for the TUI. Never throws or rejects; the\n * returned promise resolves once this snapshot and every earlier one are written.\n */\n persist(): Promise<void> {\n const snapshot: PersistedState = {\n version: 1,\n updatedAt: Date.now(),\n enabled: this.enabled,\n intervalMs: this.config.intervalMs,\n windowMs: this.config.windowMs,\n sessions: Object.fromEntries(\n this.all().map((s) => [\n s.sessionID,\n {\n eligible: s.eligible,\n modelLabel: s.modelLabel,\n lastResponseAt: s.lastResponseAt,\n idleSince: s.idleSince,\n windowEndsAt: s.windowEndsAt,\n nextPingAt: s.nextPingAt,\n intervalMs: this.config.intervalMs,\n pingsSent: s.pingsSent,\n busy: s.busy,\n warming: s.warming,\n active: s.active,\n lastPing: s.lastPing,\n } satisfies PersistedSession,\n ]),\n ),\n }\n\n const write = () => atomicWrite(stateFilePath(this.directory), JSON.stringify(snapshot))\n this.writeQueue = this.writeQueue.then(write, write)\n return this.writeQueue\n }\n}\n\nasync function atomicWrite(path: string, contents: string): Promise<void> {\n try {\n const tmp = `${path}.${process.pid}.${Date.now()}.tmp`\n await mkdir(dirname(path), { recursive: true })\n await writeFile(tmp, `${contents}\\n`, \"utf8\")\n await rename(tmp, path)\n } catch {\n // Persistence is best-effort; the TUI readout simply falls behind.\n }\n}\n","/**\n * Stable keepalive instruction appended to the system prompt.\n *\n * IMPORTANT: this text must be byte-stable and applied to *every* request for an\n * eligible model (real turns and pings alike). If it were only added on pings, the\n * system prefix would differ from real turns and the ping would miss the cache.\n * Adding it consistently keeps the cached prefix identical and lets the model\n * answer a ping with a single token instead of a paragraph or a tool call.\n */\nexport function keepaliveInstruction(pingToken: string): string {\n return [\n \"<cache_keepalive>\",\n `A user message consisting of exactly the single token \"${pingToken}\" is an`,\n \"automated cache-keepalive ping, not a real request. When you receive it,\",\n `reply with exactly \"${pingToken}\" and nothing else. Do not call any tools,`,\n \"do not explain, and do not take any action.\",\n \"</cache_keepalive>\",\n ].join(\"\\n\")\n}\n","import type { Plugin } from \"@opencode-ai/plugin\"\nimport { configureClientAuth, isSecureMode } from \"./lib/auth\"\nimport { getConfig } from \"./lib/config\"\nimport { KeepaliveEngine } from \"./lib/keepalive\"\nimport { Logger } from \"./lib/logger\"\nimport { isEligibleModel } from \"./lib/model\"\nimport { logFilePath } from \"./lib/paths\"\nimport { KeepaliveStore } from \"./lib/state\"\nimport { keepaliveInstruction } from \"./lib/system\"\n\ndeclare const __KEEPALIVE_VERSION__: string\n\nconst server: Plugin = async (ctx, options) => {\n const config = getConfig(options as Record<string, unknown> | undefined)\n const logger = new Logger(config.debug, logFilePath())\n\n if (!config.enabled) {\n logger.info(\"disabled via config\")\n return {}\n }\n\n if (isSecureMode()) configureClientAuth(ctx.client)\n\n const store = new KeepaliveStore(config, ctx.directory)\n const engine = new KeepaliveEngine(ctx.client, config, store, logger, ctx.directory)\n engine.start()\n\n const version = typeof __KEEPALIVE_VERSION__ !== \"undefined\" ? __KEEPALIVE_VERSION__ : \"dev\"\n logger.info(`v${version} ready`)\n\n const instruction = keepaliveInstruction(config.pingToken)\n\n return {\n event: async ({ event }) => {\n await engine.onEvent(event)\n },\n\n // Keep the system prefix identical between real turns and pings, and make\n // the model answer a ping with a single token.\n \"experimental.chat.system.transform\": async (input, output) => {\n if (!config.injectSystemInstruction) return\n const model = input.model as { providerID?: string; id?: string; modelID?: string }\n if (!isEligibleModel(config, model?.providerID, model?.id ?? model?.modelID)) return\n output.system.push(instruction)\n },\n\n // Backstop: if the model ignores the instruction and tries a tool during a\n // ping, block it — the ping turn is going to be reverted anyway.\n \"tool.execute.before\": async (input) => {\n if (engine.shouldBlockTools(input.sessionID)) {\n throw new Error(\"cache-keepalive: tools are disabled during a keepalive ping\")\n }\n },\n\n dispose: async () => {\n engine.stop()\n },\n }\n}\n\nexport default server\n"],"mappings":"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","names":["dirname","mkdir","rename","writeFile","dirname","mkdir","dirname","writeFile","rename"]}
|
package/dist/lib/keepalive.d.ts
CHANGED
|
@@ -5,12 +5,19 @@ import type { Logger } from "./logger";
|
|
|
5
5
|
* The keepalive engine.
|
|
6
6
|
*
|
|
7
7
|
* Lifecycle per session:
|
|
8
|
-
* 1. A real turn finishes -> `session.
|
|
8
|
+
* 1. A real turn finishes -> `session.status` idle -> arm a warm window anchored on
|
|
9
|
+
* the real response and schedule pings.
|
|
9
10
|
* 2. On each tick past `nextPingAt` (and within the window) -> `firePing`.
|
|
10
|
-
* 3. `firePing`
|
|
11
|
-
* the
|
|
12
|
-
*
|
|
13
|
-
* 4. A new real user turn
|
|
11
|
+
* 3. `firePing` confirms with the server that the session is idle, sends a `~`
|
|
12
|
+
* prompt with the last real turn's agent/model/variant (exact cached prefix),
|
|
13
|
+
* reads `usage` to confirm a cache hit, then optionally reverts the `~`/`~` turn.
|
|
14
|
+
* 4. A new real user turn, the window closing, or the cache presumed cold stops warming.
|
|
15
|
+
*
|
|
16
|
+
* Threading: all state is mutated on the single plugin event loop. opencode invokes
|
|
17
|
+
* the `event` hook without awaiting it, so async handlers interleave; per-session
|
|
18
|
+
* `arming` / `warming` flags serialize the async sections.
|
|
19
|
+
*
|
|
20
|
+
* Errors: network/API failures are logged and never thrown to opencode.
|
|
14
21
|
*/
|
|
15
22
|
export declare class KeepaliveEngine {
|
|
16
23
|
private readonly client;
|
|
@@ -27,14 +34,49 @@ export declare class KeepaliveEngine {
|
|
|
27
34
|
constructor(client: any, config: KeepaliveConfig, store: KeepaliveStore, logger: Logger, directory: string);
|
|
28
35
|
start(): void;
|
|
29
36
|
stop(): void;
|
|
30
|
-
/**
|
|
31
|
-
|
|
37
|
+
/**
|
|
38
|
+
* True while a ping turn owns the session, so tool calls must be blocked. Becomes
|
|
39
|
+
* false as soon as a real user message joins the in-flight ping run: the rest of
|
|
40
|
+
* that run is real work and needs its tools.
|
|
41
|
+
*/
|
|
42
|
+
shouldBlockTools(sessionID: string): boolean;
|
|
32
43
|
onEvent(event: any): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Track a new user message observed while a ping is in flight. Event order alone
|
|
46
|
+
* cannot tell the ping's own message from a real prompt racing it, so messages are
|
|
47
|
+
* held until their text part arrives (see `notePingRunPart`). Any message seen
|
|
48
|
+
* before the ping is sent, or a second new message, is necessarily a real turn.
|
|
49
|
+
*/
|
|
50
|
+
private notePingRunUser;
|
|
51
|
+
/**
|
|
52
|
+
* Classify a pending user message by its first text part: the ping token marks
|
|
53
|
+
* the ping's own message; any other text is a real turn joining the ping run.
|
|
54
|
+
* opencode publishes a user message's parts right after the message itself, before
|
|
55
|
+
* the run reaches any tool call.
|
|
56
|
+
*/
|
|
57
|
+
private notePingRunPart;
|
|
58
|
+
/** A real turn joined the in-flight ping: unblock tools and treat the session as busy. */
|
|
59
|
+
private interruptPing;
|
|
33
60
|
private armWindow;
|
|
61
|
+
/** Start a warm window anchored on the last real response and the last cache touch. */
|
|
62
|
+
private openWindow;
|
|
63
|
+
/** False once the cache has gone too long without a request to still be trusted warm. */
|
|
64
|
+
private cacheMayBeWarm;
|
|
34
65
|
private tick;
|
|
35
66
|
private firePing;
|
|
36
|
-
/**
|
|
67
|
+
/** True only when the server positively reports the session idle. */
|
|
68
|
+
private isIdleOnServer;
|
|
69
|
+
/** Recover the last real turn's request settings when no user event was observed. */
|
|
70
|
+
private lookupRequest;
|
|
71
|
+
private recentMessages;
|
|
72
|
+
private isPingMessage;
|
|
73
|
+
/**
|
|
74
|
+
* Remove the `~` user + `~` assistant turn so it never enters real context.
|
|
75
|
+
* Revert drops the target and everything after it (and rolls back file
|
|
76
|
+
* snapshots), so it is skipped unless only the ping's own replies follow.
|
|
77
|
+
*/
|
|
37
78
|
private revertPing;
|
|
79
|
+
private isRecentPing;
|
|
38
80
|
private resolveSession;
|
|
39
81
|
private pollControl;
|
|
40
82
|
/** Recover when a session.status idle event is missed by reconciling with the API. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keepalive.d.ts","sourceRoot":"","sources":["../../lib/keepalive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAG/C,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"keepalive.d.ts","sourceRoot":"","sources":["../../lib/keepalive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAG/C,OAAO,KAAK,EAAgB,cAAc,EAAiC,MAAM,SAAS,CAAA;AAC1F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAgBtC;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,eAAe;IASpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAZ9B,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,WAAW,CAA4C;IAC/D,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,iBAAiB,CAAQ;IACjC,OAAO,CAAC,OAAO,CAAS;gBAGH,MAAM,EAAE,GAAG,EACX,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM;IAQtC,KAAK,IAAI,IAAI;IAab,IAAI,IAAI,IAAI;IAUZ;;;;OAIG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAKtC,OAAO,CAAC,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAiGxC;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAcvB;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAcvB,0FAA0F;IAC1F,OAAO,CAAC,aAAa;YAWP,SAAS;IAuCvB,uFAAuF;IACvF,OAAO,CAAC,UAAU;IASlB,yFAAyF;IACzF,OAAO,CAAC,cAAc;YAKR,IAAI;YAgCJ,QAAQ;IAsGtB,qEAAqE;YACvD,cAAc;IAW5B,qFAAqF;YACvE,aAAa;YAeb,cAAc;IAc5B,OAAO,CAAC,aAAa;IASrB;;;;OAIG;YACW,UAAU;IAoCxB,OAAO,CAAC,YAAY;YAMN,cAAc;IAiB5B,OAAO,CAAC,WAAW;IAwBnB,sFAAsF;YACxE,eAAe;CAmBhC"}
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -7,6 +7,38 @@ export type PingRecord = {
|
|
|
7
7
|
cacheWrite: number;
|
|
8
8
|
output: number;
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Request settings of the last real user turn. A ping must replay them exactly:
|
|
12
|
+
* a prompt without explicit settings runs as opencode's default agent with no
|
|
13
|
+
* variant, which changes the request shape (missing the cache) and overwrites
|
|
14
|
+
* the session's persisted agent/model/variant.
|
|
15
|
+
*/
|
|
16
|
+
export type PingRequest = {
|
|
17
|
+
agent?: string;
|
|
18
|
+
providerID: string;
|
|
19
|
+
modelID: string;
|
|
20
|
+
variant?: string;
|
|
21
|
+
};
|
|
22
|
+
/** Bookkeeping for the ping request currently in flight. */
|
|
23
|
+
export type InflightPing = {
|
|
24
|
+
/** Epoch ms the ping attempt began (before the pre-flight status check). */
|
|
25
|
+
startedAt: number;
|
|
26
|
+
/** The prompt request has been issued. */
|
|
27
|
+
sent: boolean;
|
|
28
|
+
/** The ping's own user message, identified by its ping-token text part. */
|
|
29
|
+
messageID?: string;
|
|
30
|
+
/**
|
|
31
|
+
* New user messages seen during the ping whose text has not yet been observed.
|
|
32
|
+
* Each is classified by its first text part: the ping token marks our own
|
|
33
|
+
* message, anything else a real turn.
|
|
34
|
+
*/
|
|
35
|
+
pending: Map<string, unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* A real user message appeared while the ping was in flight. opencode joins such
|
|
38
|
+
* a prompt to the running ping loop, so the rest of the run is real work.
|
|
39
|
+
*/
|
|
40
|
+
interrupted: boolean;
|
|
41
|
+
};
|
|
10
42
|
/** Live per-session bookkeeping held by the server plugin. */
|
|
11
43
|
export type SessionKeepalive = {
|
|
12
44
|
sessionID: string;
|
|
@@ -25,15 +57,20 @@ export type SessionKeepalive = {
|
|
|
25
57
|
pingsSent: number;
|
|
26
58
|
/** A real turn is currently running. */
|
|
27
59
|
busy: boolean;
|
|
28
|
-
/** Epoch ms when `busy` was last set; used as a stuck-busy timeout backstop. */
|
|
29
|
-
busySince?: number;
|
|
30
60
|
/** A keepalive ping is currently in flight. */
|
|
31
61
|
warming: boolean;
|
|
62
|
+
/** The in-flight ping; set exactly while `warming` is true. */
|
|
63
|
+
ping?: InflightPing;
|
|
64
|
+
/** ID and creation time of the newest user message seen, to tell new turns from re-emitted old messages. */
|
|
65
|
+
lastUserID?: string;
|
|
66
|
+
lastUserAt?: number;
|
|
67
|
+
/** Settings of the last real user turn, replayed by pings. */
|
|
68
|
+
request?: PingRequest;
|
|
32
69
|
/** Within the warm window and actively scheduling pings. */
|
|
33
70
|
active: boolean;
|
|
34
71
|
/** True while `armWindow` is resolving session metadata (async guard). */
|
|
35
72
|
arming?: boolean;
|
|
36
|
-
/** Epoch ms
|
|
73
|
+
/** Epoch ms the last sent ping finished; ignores its late events and marks the last cache touch. */
|
|
37
74
|
lastPingAt?: number;
|
|
38
75
|
lastPing?: PingRecord;
|
|
39
76
|
};
|
|
@@ -72,7 +109,10 @@ export declare class KeepaliveStore {
|
|
|
72
109
|
ensure(sessionID: string): SessionKeepalive;
|
|
73
110
|
remove(sessionID: string): void;
|
|
74
111
|
all(): SessionKeepalive[];
|
|
75
|
-
/**
|
|
76
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Atomically persist a lean snapshot for the TUI. Never throws or rejects; the
|
|
114
|
+
* returned promise resolves once this snapshot and every earlier one are written.
|
|
115
|
+
*/
|
|
116
|
+
persist(): Promise<void>;
|
|
77
117
|
}
|
|
78
118
|
//# sourceMappingURL=state.d.ts.map
|
package/dist/lib/state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../lib/state.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAG/C,MAAM,MAAM,UAAU,GAAG;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,OAAO,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,8DAA8D;AAC9D,MAAM,MAAM,gBAAgB,GAAG;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,4EAA4E;IAC5E,cAAc,EAAE,MAAM,CAAA;IACtB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB,8EAA8E;IAC9E,YAAY,EAAE,MAAM,CAAA;IACpB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;IACjB,wCAAwC;IACxC,IAAI,EAAE,OAAO,CAAA;IACb
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../lib/state.ts"],"names":[],"mappings":"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"}
|