@vikrant82/opencode-cache-keepalive 0.1.2 → 0.1.4
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 +12 -9
- package/dist/index.js.map +1 -1
- package/dist/lib/control.d.ts +2 -2
- package/dist/lib/control.d.ts.map +1 -1
- package/dist/lib/keepalive.d.ts +2 -1
- package/dist/lib/keepalive.d.ts.map +1 -1
- package/dist/lib/paths.d.ts +6 -2
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/tui/commands.d.ts.map +1 -1
- package/lib/control.ts +4 -4
- package/lib/keepalive.ts +3 -2
- package/lib/paths.ts +8 -3
- package/lib/tui/commands.ts +9 -4
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -109,18 +109,19 @@ function stateDirectoryPath() {
|
|
|
109
109
|
"keepalive"
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
|
-
function controlFilePath() {
|
|
113
|
-
|
|
112
|
+
function controlFilePath(directory) {
|
|
113
|
+
const key = createHash("sha256").update(directory).digest("hex").slice(0, 16);
|
|
114
|
+
return join(stateDirectoryPath(), `control-${key}.json`);
|
|
114
115
|
}
|
|
115
116
|
function logFilePath() {
|
|
116
117
|
return join(stateDirectoryPath(), "server.log");
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
// lib/control.ts
|
|
120
|
-
function readControl() {
|
|
121
|
+
function readControl(directory) {
|
|
121
122
|
try {
|
|
122
123
|
const value = JSON.parse(
|
|
123
|
-
readFileSync(controlFilePath(), "utf8")
|
|
124
|
+
readFileSync(controlFilePath(directory), "utf8")
|
|
124
125
|
);
|
|
125
126
|
if (value.version !== 1 || typeof value.enabled !== "boolean") return void 0;
|
|
126
127
|
if (typeof value.updatedAt !== "number") return void 0;
|
|
@@ -148,12 +149,13 @@ var STATUS_POLL_MS = 2e3;
|
|
|
148
149
|
var PING_STATUS_GRACE_MS = 5e3;
|
|
149
150
|
var BUSY_TIMEOUT_MS = 3e5;
|
|
150
151
|
var KeepaliveEngine = class {
|
|
151
|
-
constructor(client, config, store, logger) {
|
|
152
|
+
constructor(client, config, store, logger, directory) {
|
|
152
153
|
this.client = client;
|
|
153
154
|
this.config = config;
|
|
154
155
|
this.store = store;
|
|
155
156
|
this.logger = logger;
|
|
156
|
-
|
|
157
|
+
this.directory = directory;
|
|
158
|
+
const control = readControl(directory);
|
|
157
159
|
this.enabled = control?.enabled ?? config.enabled;
|
|
158
160
|
this.controlUpdatedAt = control?.updatedAt ?? 0;
|
|
159
161
|
this.store.setEnabled(this.enabled);
|
|
@@ -162,6 +164,7 @@ var KeepaliveEngine = class {
|
|
|
162
164
|
config;
|
|
163
165
|
store;
|
|
164
166
|
logger;
|
|
167
|
+
directory;
|
|
165
168
|
timer;
|
|
166
169
|
controlTimer;
|
|
167
170
|
statusTimer;
|
|
@@ -388,7 +391,7 @@ var KeepaliveEngine = class {
|
|
|
388
391
|
}
|
|
389
392
|
}
|
|
390
393
|
pollControl() {
|
|
391
|
-
const control = readControl();
|
|
394
|
+
const control = readControl(this.directory);
|
|
392
395
|
if (!control || control.updatedAt <= this.controlUpdatedAt) return;
|
|
393
396
|
this.controlUpdatedAt = control.updatedAt;
|
|
394
397
|
if (control.enabled === this.enabled) return;
|
|
@@ -591,9 +594,9 @@ var server = async (ctx, options) => {
|
|
|
591
594
|
}
|
|
592
595
|
if (isSecureMode()) configureClientAuth(ctx.client);
|
|
593
596
|
const store = new KeepaliveStore(config, ctx.directory);
|
|
594
|
-
const engine = new KeepaliveEngine(ctx.client, config, store, logger);
|
|
597
|
+
const engine = new KeepaliveEngine(ctx.client, config, store, logger, ctx.directory);
|
|
595
598
|
engine.start();
|
|
596
|
-
const version = true ? "0.1.
|
|
599
|
+
const version = true ? "0.1.4" : "dev";
|
|
597
600
|
logger.info(`v${version} ready`);
|
|
598
601
|
const instruction = keepaliveInstruction(config.pingToken);
|
|
599
602
|
return {
|
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(): KeepaliveControl | undefined {\n try {\n const value = JSON.parse(\n readFileSync(controlFilePath(), \"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): Promise<KeepaliveControl> {\n const value: KeepaliveControl = {\n version: 1,\n enabled,\n updatedAt: Date.now(),\n }\n const path = controlFilePath()\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/** Runtime on/off control written by the TUI and read by the server plugin. */\nexport function controlFilePath(): string {\n return join(stateDirectoryPath(), \"control.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 ) {\n const control = readControl()\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()\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)\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 { 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"]}
|
package/dist/lib/control.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ export type KeepaliveControl = {
|
|
|
3
3
|
enabled: boolean;
|
|
4
4
|
updatedAt: number;
|
|
5
5
|
};
|
|
6
|
-
export declare function readControl(): KeepaliveControl | undefined;
|
|
7
|
-
export declare function writeControl(enabled: boolean): Promise<KeepaliveControl>;
|
|
6
|
+
export declare function readControl(directory: string): KeepaliveControl | undefined;
|
|
7
|
+
export declare function writeControl(enabled: boolean, directory: string): Promise<KeepaliveControl>;
|
|
8
8
|
//# sourceMappingURL=control.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../../lib/control.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,gBAAgB,GAAG;IAC3B,OAAO,EAAE,CAAC,CAAA;IACV,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,wBAAgB,WAAW,
|
|
1
|
+
{"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../../lib/control.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,gBAAgB,GAAG;IAC3B,OAAO,EAAE,CAAC,CAAA;IACV,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAW3E;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAYjG"}
|
package/dist/lib/keepalive.d.ts
CHANGED
|
@@ -17,13 +17,14 @@ export declare class KeepaliveEngine {
|
|
|
17
17
|
private readonly config;
|
|
18
18
|
private readonly store;
|
|
19
19
|
private readonly logger;
|
|
20
|
+
private readonly directory;
|
|
20
21
|
private timer;
|
|
21
22
|
private controlTimer;
|
|
22
23
|
private statusTimer;
|
|
23
24
|
private controlUpdatedAt;
|
|
24
25
|
private reconcilingStatus;
|
|
25
26
|
private enabled;
|
|
26
|
-
constructor(client: any, config: KeepaliveConfig, store: KeepaliveStore, logger: Logger);
|
|
27
|
+
constructor(client: any, config: KeepaliveConfig, store: KeepaliveStore, logger: Logger, directory: string);
|
|
27
28
|
start(): void;
|
|
28
29
|
stop(): void;
|
|
29
30
|
/** True while a ping is in flight — used to block tool calls during a ping. */
|
|
@@ -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,EAAE,cAAc,EAAoB,MAAM,SAAS,CAAA;AAC/D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAUtC;;;;;;;;;;GAUG;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;
|
|
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,EAAE,cAAc,EAAoB,MAAM,SAAS,CAAA;AAC/D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAUtC;;;;;;;;;;GAUG;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,+EAA+E;IAC/E,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAI/B,OAAO,CAAC,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;YAkF1B,SAAS;YAqCT,IAAI;YA6BJ,QAAQ;IAkDtB,gFAAgF;YAClE,UAAU;YAsCV,cAAc;IAiB5B,OAAO,CAAC,WAAW;IA6BnB,sFAAsF;YACxE,eAAe;CAmBhC"}
|
package/dist/lib/paths.d.ts
CHANGED
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function stateFilePath(directory: string): string;
|
|
10
10
|
export declare function stateDirectoryPath(): string;
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Runtime on/off control written by the TUI and read by the server plugin.
|
|
13
|
+
* Scoped per directory (like the state file) so toggling keepalive in one
|
|
14
|
+
* project does not affect every other open opencode session.
|
|
15
|
+
*/
|
|
16
|
+
export declare function controlFilePath(directory: string): string;
|
|
13
17
|
/** Append-only log file for the server plugin — never stdout, which corrupts the TUI. */
|
|
14
18
|
export declare function logFilePath(): string;
|
|
15
19
|
//# sourceMappingURL=paths.d.ts.map
|
package/dist/lib/paths.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../lib/paths.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAQ3C;AAED
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../lib/paths.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAQ3C;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,yFAAyF;AACzF,wBAAgB,WAAW,IAAI,MAAM,CAEpC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../lib/tui/commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAG3D,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../lib/tui/commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAG3D,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAmDjE"}
|
package/lib/control.ts
CHANGED
|
@@ -9,10 +9,10 @@ export type KeepaliveControl = {
|
|
|
9
9
|
updatedAt: number
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function readControl(): KeepaliveControl | undefined {
|
|
12
|
+
export function readControl(directory: string): KeepaliveControl | undefined {
|
|
13
13
|
try {
|
|
14
14
|
const value = JSON.parse(
|
|
15
|
-
readFileSync(controlFilePath(), "utf8"),
|
|
15
|
+
readFileSync(controlFilePath(directory), "utf8"),
|
|
16
16
|
) as Partial<KeepaliveControl>
|
|
17
17
|
if (value.version !== 1 || typeof value.enabled !== "boolean") return undefined
|
|
18
18
|
if (typeof value.updatedAt !== "number") return undefined
|
|
@@ -22,13 +22,13 @@ export function readControl(): KeepaliveControl | undefined {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export async function writeControl(enabled: boolean): Promise<KeepaliveControl> {
|
|
25
|
+
export async function writeControl(enabled: boolean, directory: string): Promise<KeepaliveControl> {
|
|
26
26
|
const value: KeepaliveControl = {
|
|
27
27
|
version: 1,
|
|
28
28
|
enabled,
|
|
29
29
|
updatedAt: Date.now(),
|
|
30
30
|
}
|
|
31
|
-
const path = controlFilePath()
|
|
31
|
+
const path = controlFilePath(directory)
|
|
32
32
|
const tmp = `${path}.${process.pid}.${value.updatedAt}.tmp`
|
|
33
33
|
await mkdir(dirname(path), { recursive: true })
|
|
34
34
|
await writeFile(tmp, `${JSON.stringify(value)}\n`, "utf8")
|
package/lib/keepalive.ts
CHANGED
|
@@ -36,8 +36,9 @@ export class KeepaliveEngine {
|
|
|
36
36
|
private readonly config: KeepaliveConfig,
|
|
37
37
|
private readonly store: KeepaliveStore,
|
|
38
38
|
private readonly logger: Logger,
|
|
39
|
+
private readonly directory: string,
|
|
39
40
|
) {
|
|
40
|
-
const control = readControl()
|
|
41
|
+
const control = readControl(directory)
|
|
41
42
|
this.enabled = control?.enabled ?? config.enabled
|
|
42
43
|
this.controlUpdatedAt = control?.updatedAt ?? 0
|
|
43
44
|
this.store.setEnabled(this.enabled)
|
|
@@ -326,7 +327,7 @@ export class KeepaliveEngine {
|
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
private pollControl(): void {
|
|
329
|
-
const control = readControl()
|
|
330
|
+
const control = readControl(this.directory)
|
|
330
331
|
if (!control || control.updatedAt <= this.controlUpdatedAt) return
|
|
331
332
|
this.controlUpdatedAt = control.updatedAt
|
|
332
333
|
if (control.enabled === this.enabled) return
|
package/lib/paths.ts
CHANGED
|
@@ -25,9 +25,14 @@ export function stateDirectoryPath(): string {
|
|
|
25
25
|
)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Runtime on/off control written by the TUI and read by the server plugin.
|
|
30
|
+
* Scoped per directory (like the state file) so toggling keepalive in one
|
|
31
|
+
* project does not affect every other open opencode session.
|
|
32
|
+
*/
|
|
33
|
+
export function controlFilePath(directory: string): string {
|
|
34
|
+
const key = createHash("sha256").update(directory).digest("hex").slice(0, 16)
|
|
35
|
+
return join(stateDirectoryPath(), `control-${key}.json`)
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
/** Append-only log file for the server plugin — never stdout, which corrupts the TUI. */
|
package/lib/tui/commands.ts
CHANGED
|
@@ -2,9 +2,14 @@ import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
|
2
2
|
import { readControl, writeControl } from "../control"
|
|
3
3
|
|
|
4
4
|
export function registerKeepaliveCommands(api: TuiPluginApi): void {
|
|
5
|
+
// The control file is scoped per directory, so toggling keepalive here only
|
|
6
|
+
// affects this project's server plugin — not other open opencode sessions.
|
|
7
|
+
// Read lazily: state paths may not be populated at registration time.
|
|
8
|
+
const directory = () => api.state.path.directory
|
|
9
|
+
|
|
5
10
|
const setEnabled = async (enabled: boolean) => {
|
|
6
11
|
try {
|
|
7
|
-
await writeControl(enabled)
|
|
12
|
+
await writeControl(enabled, directory())
|
|
8
13
|
api.ui.toast({
|
|
9
14
|
variant: enabled ? "success" : "warning",
|
|
10
15
|
title: "Cache keepalive",
|
|
@@ -27,7 +32,7 @@ export function registerKeepaliveCommands(api: TuiPluginApi): void {
|
|
|
27
32
|
category: "Keepalive",
|
|
28
33
|
namespace: "palette",
|
|
29
34
|
slashName: "keepalive-toggle",
|
|
30
|
-
run: () => setEnabled(!currentEnabled()),
|
|
35
|
+
run: () => setEnabled(!currentEnabled(directory())),
|
|
31
36
|
},
|
|
32
37
|
{
|
|
33
38
|
name: "keepalive.on",
|
|
@@ -49,6 +54,6 @@ export function registerKeepaliveCommands(api: TuiPluginApi): void {
|
|
|
49
54
|
})
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
function currentEnabled(): boolean {
|
|
53
|
-
return readControl()?.enabled ?? true
|
|
57
|
+
function currentEnabled(directory: string): boolean {
|
|
58
|
+
return readControl(directory)?.enabled ?? true
|
|
54
59
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@vikrant82/opencode-cache-keepalive",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "OpenCode plugin that keeps provider prompt caches warm by sending invisible keepalive pings, with a live TUI readout",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"license": "AGPL-3.0-or-later",
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@opencode-ai/plugin": ">=1.4.3",
|
|
47
|
-
"@opentui/core": "
|
|
48
|
-
"@opentui/solid": "
|
|
47
|
+
"@opentui/core": ">=0.4.2 <1.0.0",
|
|
48
|
+
"@opentui/solid": ">=0.4.2 <1.0.0",
|
|
49
49
|
"solid-js": "^1.9.12"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|