@vikrant82/opencode-cache-keepalive 0.1.5 → 0.1.6
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/README.md +4 -1
- package/dist/index.js +65 -26
- package/dist/index.js.map +1 -1
- package/dist/lib/control.d.ts +34 -1
- package/dist/lib/control.d.ts.map +1 -1
- package/dist/lib/keepalive.d.ts +3 -0
- package/dist/lib/keepalive.d.ts.map +1 -1
- package/dist/lib/state.d.ts +3 -0
- package/dist/lib/state.d.ts.map +1 -1
- package/dist/lib/tui/commands.d.ts +7 -0
- package/dist/lib/tui/commands.d.ts.map +1 -1
- package/dist/lib/tui/footer.d.ts.map +1 -1
- package/dist/lib/tui/format.d.ts +8 -0
- package/dist/lib/tui/format.d.ts.map +1 -1
- package/lib/control.ts +96 -11
- package/lib/keepalive.ts +49 -20
- package/lib/state.ts +9 -2
- package/lib/tui/commands.tsx +184 -0
- package/lib/tui/footer.tsx +33 -6
- package/lib/tui/format.ts +36 -0
- package/package.json +1 -1
- package/lib/tui/commands.ts +0 -59
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Keep LLM prompt caches warm during idle periods with invisible pings and a live
|
|
|
18
18
|
- **Live TUI footer**: idle time, pings sent/left, last ping cache hit/miss with token counts
|
|
19
19
|
- **Synthetic ping reversion**: removes `~`/`~` turns from conversation history
|
|
20
20
|
- **Runtime toggle**: `/keepalive-toggle`, `/keepalive-on`, `/keepalive-off` slash commands
|
|
21
|
+
- **Runtime interval override**: set a per-project ping interval with presets, custom durations, or reset to the plugin default
|
|
21
22
|
- **Secure mode support**: works with `OPENCODE_SERVER_PASSWORD`
|
|
22
23
|
- **Configurable provider/model allowlists**
|
|
23
24
|
|
|
@@ -85,7 +86,7 @@ All options can be set via plugin config in `opencode.json` or environment varia
|
|
|
85
86
|
The plugin registers a sidebar footer showing live keepalive status:
|
|
86
87
|
|
|
87
88
|
```
|
|
88
|
-
keepalive armed
|
|
89
|
+
keepalive armed every 4m 30s
|
|
89
90
|
idle 03:42 · sent 2 · left 10
|
|
90
91
|
✓ hit input 12.4k · read 11.8k · write 0
|
|
91
92
|
```
|
|
@@ -93,6 +94,7 @@ idle 03:42 · sent 2 · left 10
|
|
|
93
94
|
Metrics:
|
|
94
95
|
|
|
95
96
|
- **idle**: Time since last real assistant response
|
|
97
|
+
- **every**: Effective ping interval, including any runtime project override
|
|
96
98
|
- **sent**: Ping attempts in current warm window (includes failures)
|
|
97
99
|
- **left**: Estimated pings remaining before window expires
|
|
98
100
|
- **hit/miss**: Cache hit status of last ping with token breakdown
|
|
@@ -102,6 +104,7 @@ Slash commands (available in TUI palette):
|
|
|
102
104
|
- `/keepalive-toggle` — Toggle keepalive on/off at runtime
|
|
103
105
|
- `/keepalive-on` — Enable keepalive
|
|
104
106
|
- `/keepalive-off` — Disable keepalive
|
|
107
|
+
- `/keepalive-interval` — Set this project's ping interval at runtime (presets for 5-minute and 30-minute caches, custom durations, or reset to the plugin default); persists per project until changed
|
|
105
108
|
|
|
106
109
|
## How It Works
|
|
107
110
|
|
package/dist/index.js
CHANGED
|
@@ -88,6 +88,7 @@ function numEnv(raw, fallback) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
// lib/control.ts
|
|
91
|
+
import { randomUUID } from "crypto";
|
|
91
92
|
import { readFileSync } from "fs";
|
|
92
93
|
import { mkdir, rename, writeFile } from "fs/promises";
|
|
93
94
|
import { dirname } from "path";
|
|
@@ -118,18 +119,27 @@ function logFilePath() {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
// lib/control.ts
|
|
122
|
+
var MIN_INTERVAL_MS = 6e4;
|
|
123
|
+
var MAX_INTERVAL_MS = 24 * 36e5;
|
|
124
|
+
function isValidInterval(ms) {
|
|
125
|
+
return typeof ms === "number" && Number.isFinite(ms) && ms >= MIN_INTERVAL_MS && ms <= MAX_INTERVAL_MS;
|
|
126
|
+
}
|
|
121
127
|
function readControl(directory) {
|
|
122
128
|
try {
|
|
123
|
-
const value = JSON.parse(
|
|
124
|
-
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
const value = JSON.parse(readFileSync(controlFilePath(directory), "utf8"));
|
|
130
|
+
if (value?.version !== 1 || typeof value.updatedAt !== "number") return void 0;
|
|
131
|
+
if (value.enabled !== void 0 && typeof value.enabled !== "boolean") return void 0;
|
|
132
|
+
return {
|
|
133
|
+
version: 1,
|
|
134
|
+
updatedAt: value.updatedAt,
|
|
135
|
+
...typeof value.enabled === "boolean" ? { enabled: value.enabled } : {},
|
|
136
|
+
...isValidInterval(value.intervalMs) ? { intervalMs: value.intervalMs } : {}
|
|
137
|
+
};
|
|
129
138
|
} catch {
|
|
130
139
|
return void 0;
|
|
131
140
|
}
|
|
132
141
|
}
|
|
142
|
+
var updateQueue = Promise.resolve();
|
|
133
143
|
|
|
134
144
|
// lib/model.ts
|
|
135
145
|
function isEligibleModel(config, providerID, modelID) {
|
|
@@ -158,8 +168,10 @@ var KeepaliveEngine = class {
|
|
|
158
168
|
this.directory = directory;
|
|
159
169
|
const control = readControl(directory);
|
|
160
170
|
this.enabled = control?.enabled ?? config.enabled;
|
|
171
|
+
this.intervalOverride = control?.intervalMs;
|
|
161
172
|
this.controlUpdatedAt = control?.updatedAt ?? 0;
|
|
162
173
|
this.store.setEnabled(this.enabled);
|
|
174
|
+
this.store.setIntervalMs(this.intervalMs);
|
|
163
175
|
}
|
|
164
176
|
client;
|
|
165
177
|
config;
|
|
@@ -172,6 +184,11 @@ var KeepaliveEngine = class {
|
|
|
172
184
|
controlUpdatedAt = 0;
|
|
173
185
|
reconcilingStatus = false;
|
|
174
186
|
enabled;
|
|
187
|
+
intervalOverride;
|
|
188
|
+
/** Effective ping interval: the runtime override from the control file, else config. */
|
|
189
|
+
get intervalMs() {
|
|
190
|
+
return this.intervalOverride ?? this.config.intervalMs;
|
|
191
|
+
}
|
|
175
192
|
start() {
|
|
176
193
|
if (this.timer) return;
|
|
177
194
|
this.timer = setInterval(() => void this.tick(), TICK_MS);
|
|
@@ -179,7 +196,7 @@ var KeepaliveEngine = class {
|
|
|
179
196
|
this.statusTimer = setInterval(() => void this.reconcileStatus(), STATUS_POLL_MS);
|
|
180
197
|
this.store.persist();
|
|
181
198
|
this.logger.info(
|
|
182
|
-
`started (${this.enabled ? "enabled" : "disabled"}, interval=${Math.round(this.
|
|
199
|
+
`started (${this.enabled ? "enabled" : "disabled"}, interval=${Math.round(this.intervalMs / 1e3)}s, window=${Math.round(this.config.windowMs / 6e4)}m)`
|
|
183
200
|
);
|
|
184
201
|
}
|
|
185
202
|
stop() {
|
|
@@ -351,7 +368,7 @@ var KeepaliveEngine = class {
|
|
|
351
368
|
openWindow(s, now) {
|
|
352
369
|
s.idleSince = now;
|
|
353
370
|
s.windowEndsAt = s.lastResponseAt + this.config.windowMs;
|
|
354
|
-
s.nextPingAt = Math.max(now, lastCacheTouch(s) + this.
|
|
371
|
+
s.nextPingAt = Math.max(now, lastCacheTouch(s) + this.intervalMs);
|
|
355
372
|
s.pingsSent = 0;
|
|
356
373
|
s.lastPing = void 0;
|
|
357
374
|
s.active = true;
|
|
@@ -359,7 +376,7 @@ var KeepaliveEngine = class {
|
|
|
359
376
|
/** False once the cache has gone too long without a request to still be trusted warm. */
|
|
360
377
|
cacheMayBeWarm(s, now) {
|
|
361
378
|
const touch = lastCacheTouch(s);
|
|
362
|
-
return touch > 0 && now - touch <= COLD_AFTER_INTERVALS * this.
|
|
379
|
+
return touch > 0 && now - touch <= COLD_AFTER_INTERVALS * this.intervalMs;
|
|
363
380
|
}
|
|
364
381
|
async tick() {
|
|
365
382
|
if (!this.enabled) return;
|
|
@@ -455,8 +472,8 @@ var KeepaliveEngine = class {
|
|
|
455
472
|
const now = Date.now();
|
|
456
473
|
if (ping.sent) {
|
|
457
474
|
s.lastPingAt = now;
|
|
458
|
-
const next = s.nextPingAt + jitter(this.
|
|
459
|
-
s.nextPingAt = next > now ? next : now + jitter(this.
|
|
475
|
+
const next = s.nextPingAt + jitter(this.intervalMs);
|
|
476
|
+
s.nextPingAt = next > now ? next : now + jitter(this.intervalMs);
|
|
460
477
|
}
|
|
461
478
|
this.store.persist();
|
|
462
479
|
if (ping.sent && ping.interrupted) {
|
|
@@ -566,21 +583,37 @@ var KeepaliveEngine = class {
|
|
|
566
583
|
const control = readControl(this.directory);
|
|
567
584
|
if (!control || control.updatedAt <= this.controlUpdatedAt) return;
|
|
568
585
|
this.controlUpdatedAt = control.updatedAt;
|
|
569
|
-
if (control.enabled === this.enabled) return;
|
|
570
|
-
this.enabled = control.enabled;
|
|
571
|
-
this.store.setEnabled(control.enabled);
|
|
572
586
|
const now = Date.now();
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
587
|
+
let changed = false;
|
|
588
|
+
if (control.intervalMs !== this.intervalOverride) {
|
|
589
|
+
this.intervalOverride = control.intervalMs;
|
|
590
|
+
this.store.setIntervalMs(this.intervalMs);
|
|
591
|
+
for (const session of this.store.all()) {
|
|
592
|
+
if (session.active && !session.warming)
|
|
593
|
+
session.nextPingAt = Math.max(now, lastCacheTouch(session) + this.intervalMs);
|
|
577
594
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
595
|
+
this.logger.info(
|
|
596
|
+
`runtime interval ${Math.round(this.intervalMs / 1e3)}s` + (control.intervalMs === void 0 ? " (config)" : "")
|
|
597
|
+
);
|
|
598
|
+
changed = true;
|
|
581
599
|
}
|
|
582
|
-
this.
|
|
583
|
-
|
|
600
|
+
const enabled = control.enabled ?? this.config.enabled;
|
|
601
|
+
if (enabled !== this.enabled) {
|
|
602
|
+
this.enabled = enabled;
|
|
603
|
+
this.store.setEnabled(enabled);
|
|
604
|
+
for (const session of this.store.all()) {
|
|
605
|
+
if (!enabled) {
|
|
606
|
+
session.active = false;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (!session.eligible || session.busy || session.warming) continue;
|
|
610
|
+
if (!session.lastResponseAt || !this.cacheMayBeWarm(session, now)) continue;
|
|
611
|
+
this.openWindow(session, now);
|
|
612
|
+
}
|
|
613
|
+
this.logger.info(`runtime ${enabled ? "enabled" : "disabled"}`);
|
|
614
|
+
changed = true;
|
|
615
|
+
}
|
|
616
|
+
if (changed) this.store.persist();
|
|
584
617
|
}
|
|
585
618
|
/** Recover when a session.status idle event is missed by reconciling with the API. */
|
|
586
619
|
async reconcileStatus() {
|
|
@@ -688,15 +721,21 @@ var KeepaliveStore = class {
|
|
|
688
721
|
this.config = config;
|
|
689
722
|
this.directory = directory;
|
|
690
723
|
this.enabled = config.enabled;
|
|
724
|
+
this.intervalMs = config.intervalMs;
|
|
691
725
|
}
|
|
692
726
|
config;
|
|
693
727
|
directory;
|
|
694
728
|
sessions = /* @__PURE__ */ new Map();
|
|
695
729
|
writeQueue = Promise.resolve();
|
|
696
730
|
enabled;
|
|
731
|
+
intervalMs;
|
|
697
732
|
setEnabled(enabled) {
|
|
698
733
|
this.enabled = enabled;
|
|
699
734
|
}
|
|
735
|
+
/** Effective ping interval (runtime override or config) reported to the TUI. */
|
|
736
|
+
setIntervalMs(intervalMs) {
|
|
737
|
+
this.intervalMs = intervalMs;
|
|
738
|
+
}
|
|
700
739
|
get(sessionID) {
|
|
701
740
|
return this.sessions.get(sessionID);
|
|
702
741
|
}
|
|
@@ -733,7 +772,7 @@ var KeepaliveStore = class {
|
|
|
733
772
|
version: 1,
|
|
734
773
|
updatedAt: Date.now(),
|
|
735
774
|
enabled: this.enabled,
|
|
736
|
-
intervalMs: this.
|
|
775
|
+
intervalMs: this.intervalMs,
|
|
737
776
|
windowMs: this.config.windowMs,
|
|
738
777
|
sessions: Object.fromEntries(
|
|
739
778
|
this.all().map((s) => [
|
|
@@ -745,7 +784,7 @@ var KeepaliveStore = class {
|
|
|
745
784
|
idleSince: s.idleSince,
|
|
746
785
|
windowEndsAt: s.windowEndsAt,
|
|
747
786
|
nextPingAt: s.nextPingAt,
|
|
748
|
-
intervalMs: this.
|
|
787
|
+
intervalMs: this.intervalMs,
|
|
749
788
|
pingsSent: s.pingsSent,
|
|
750
789
|
busy: s.busy,
|
|
751
790
|
warming: s.warming,
|
|
@@ -795,7 +834,7 @@ var server = async (ctx, options) => {
|
|
|
795
834
|
const store = new KeepaliveStore(config, ctx.directory);
|
|
796
835
|
const engine = new KeepaliveEngine(ctx.client, config, store, logger, ctx.directory);
|
|
797
836
|
engine.start();
|
|
798
|
-
const version = true ? "0.1.
|
|
837
|
+
const version = true ? "0.1.6" : "dev";
|
|
799
838
|
logger.info(`v${version} ready`);
|
|
800
839
|
const instruction = keepaliveInstruction(config.pingToken);
|
|
801
840
|
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(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"]}
|
|
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 { randomUUID } from \"node:crypto\"\nimport { readFileSync } from \"node:fs\"\nimport { mkdir, rename, writeFile } from \"node:fs/promises\"\nimport { dirname } from \"node:path\"\nimport { controlFilePath } from \"./paths\"\n\n/** Smallest accepted runtime ping interval; below this the 15 s scheduler tick dominates. */\nexport const MIN_INTERVAL_MS = 60_000\n/** Largest accepted runtime ping interval (sanity bound; no provider cache outlives it). */\nexport const MAX_INTERVAL_MS = 24 * 3_600_000\n\n/**\n * Per-project runtime overrides written by the TUI and polled by the server plugin.\n * Absent fields mean \"use the plugin configuration\". Readers ignore unknown fields,\n * so older servers still honour `enabled` and ignore `intervalMs`.\n */\nexport type KeepaliveControl = {\n version: 1\n /** Runtime on/off override. */\n enabled?: boolean\n /** Runtime ping interval override in milliseconds. */\n intervalMs?: number\n /** Epoch ms of the last write; strictly increasing so every write is observed. */\n updatedAt: number\n}\n\n/** Changes to apply to the control file; `intervalMs: null` clears the override. */\nexport type ControlPatch = {\n enabled?: boolean\n intervalMs?: number | null\n}\n\nexport function isValidInterval(ms: unknown): ms is number {\n return (\n typeof ms === \"number\" &&\n Number.isFinite(ms) &&\n ms >= MIN_INTERVAL_MS &&\n ms <= MAX_INTERVAL_MS\n )\n}\n\n/**\n * Read the directory's control file. Returns undefined when it is missing or\n * malformed; an out-of-range interval override is dropped rather than rejecting\n * the whole file. Never throws.\n */\nexport function readControl(directory: string): KeepaliveControl | undefined {\n try {\n const value = JSON.parse(readFileSync(controlFilePath(directory), \"utf8\")) as Record<\n string,\n unknown\n >\n if (value?.version !== 1 || typeof value.updatedAt !== \"number\") return undefined\n if (value.enabled !== undefined && typeof value.enabled !== \"boolean\") return undefined\n return {\n version: 1,\n updatedAt: value.updatedAt,\n ...(typeof value.enabled === \"boolean\" ? { enabled: value.enabled } : {}),\n ...(isValidInterval(value.intervalMs) ? { intervalMs: value.intervalMs } : {}),\n }\n } catch {\n return undefined\n }\n}\n\n/**\n * Merge `patch` into the directory's control file and write it atomically. Fields\n * not in the patch keep their current value.\n *\n * @throws RangeError when `patch.intervalMs` is outside\n * [MIN_INTERVAL_MS, MAX_INTERVAL_MS]; filesystem errors propagate.\n */\nexport async function updateControl(\n directory: string,\n patch: ControlPatch,\n): Promise<KeepaliveControl> {\n if (\n patch.intervalMs !== undefined &&\n patch.intervalMs !== null &&\n !isValidInterval(patch.intervalMs)\n ) {\n throw new RangeError(\n `Ping interval must be between ${MIN_INTERVAL_MS / 1000}s and ${MAX_INTERVAL_MS / 3_600_000}h`,\n )\n }\n\n // Serialize read-merge-write within this process so concurrent updates cannot\n // drop each other's fields. Across processes the atomic rename keeps the file\n // valid; the last writer wins.\n const run = updateQueue.then(() => writeMerged(directory, patch))\n updateQueue = run.then(\n () => undefined,\n () => undefined,\n )\n return run\n}\n\nlet updateQueue: Promise<void> = Promise.resolve()\n\nasync function writeMerged(directory: string, patch: ControlPatch): Promise<KeepaliveControl> {\n const current = readControl(directory)\n const enabled = patch.enabled ?? current?.enabled\n const intervalMs =\n patch.intervalMs === null ? undefined : (patch.intervalMs ?? current?.intervalMs)\n const value: KeepaliveControl = {\n version: 1,\n ...(enabled !== undefined ? { enabled } : {}),\n ...(intervalMs !== undefined ? { intervalMs } : {}),\n updatedAt: Math.max(Date.now(), (current?.updatedAt ?? 0) + 1),\n }\n const path = controlFilePath(directory)\n const tmp = `${path}.${process.pid}.${randomUUID()}.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\n/** Set the runtime on/off override, preserving any interval override. */\nexport function writeControl(enabled: boolean, directory: string): Promise<KeepaliveControl> {\n return updateControl(directory, { enabled })\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 private intervalOverride: number | undefined\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.intervalOverride = control?.intervalMs\n this.controlUpdatedAt = control?.updatedAt ?? 0\n this.store.setEnabled(this.enabled)\n this.store.setIntervalMs(this.intervalMs)\n }\n\n /** Effective ping interval: the runtime override from the control file, else config. */\n private get intervalMs(): number {\n return this.intervalOverride ?? this.config.intervalMs\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.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.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.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.intervalMs)\n s.nextPingAt = next > now ? next : now + jitter(this.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 const now = Date.now()\n let changed = false\n\n if (control.intervalMs !== this.intervalOverride) {\n this.intervalOverride = control.intervalMs\n this.store.setIntervalMs(this.intervalMs)\n // Re-anchor pending pings on the new cadence. An in-flight ping schedules\n // its successor with the new interval when it finishes.\n for (const session of this.store.all()) {\n if (session.active && !session.warming)\n session.nextPingAt = Math.max(now, lastCacheTouch(session) + this.intervalMs)\n }\n this.logger.info(\n `runtime interval ${Math.round(this.intervalMs / 1000)}s` +\n (control.intervalMs === undefined ? \" (config)\" : \"\"),\n )\n changed = true\n }\n\n const enabled = control.enabled ?? this.config.enabled\n if (enabled !== this.enabled) {\n this.enabled = enabled\n this.store.setEnabled(enabled)\n for (const session of this.store.all()) {\n if (!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.logger.info(`runtime ${enabled ? \"enabled\" : \"disabled\"}`)\n changed = true\n }\n\n if (changed) this.store.persist()\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 private intervalMs: number\n\n constructor(\n private readonly config: KeepaliveConfig,\n private readonly directory: string,\n ) {\n this.enabled = config.enabled\n this.intervalMs = config.intervalMs\n }\n\n setEnabled(enabled: boolean): void {\n this.enabled = enabled\n }\n\n /** Effective ping interval (runtime override or config) reported to the TUI. */\n setIntervalMs(intervalMs: number): void {\n this.intervalMs = intervalMs\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.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.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/control.d.ts
CHANGED
|
@@ -1,8 +1,41 @@
|
|
|
1
|
+
/** Smallest accepted runtime ping interval; below this the 15 s scheduler tick dominates. */
|
|
2
|
+
export declare const MIN_INTERVAL_MS = 60000;
|
|
3
|
+
/** Largest accepted runtime ping interval (sanity bound; no provider cache outlives it). */
|
|
4
|
+
export declare const MAX_INTERVAL_MS: number;
|
|
5
|
+
/**
|
|
6
|
+
* Per-project runtime overrides written by the TUI and polled by the server plugin.
|
|
7
|
+
* Absent fields mean "use the plugin configuration". Readers ignore unknown fields,
|
|
8
|
+
* so older servers still honour `enabled` and ignore `intervalMs`.
|
|
9
|
+
*/
|
|
1
10
|
export type KeepaliveControl = {
|
|
2
11
|
version: 1;
|
|
3
|
-
|
|
12
|
+
/** Runtime on/off override. */
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
/** Runtime ping interval override in milliseconds. */
|
|
15
|
+
intervalMs?: number;
|
|
16
|
+
/** Epoch ms of the last write; strictly increasing so every write is observed. */
|
|
4
17
|
updatedAt: number;
|
|
5
18
|
};
|
|
19
|
+
/** Changes to apply to the control file; `intervalMs: null` clears the override. */
|
|
20
|
+
export type ControlPatch = {
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
intervalMs?: number | null;
|
|
23
|
+
};
|
|
24
|
+
export declare function isValidInterval(ms: unknown): ms is number;
|
|
25
|
+
/**
|
|
26
|
+
* Read the directory's control file. Returns undefined when it is missing or
|
|
27
|
+
* malformed; an out-of-range interval override is dropped rather than rejecting
|
|
28
|
+
* the whole file. Never throws.
|
|
29
|
+
*/
|
|
6
30
|
export declare function readControl(directory: string): KeepaliveControl | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Merge `patch` into the directory's control file and write it atomically. Fields
|
|
33
|
+
* not in the patch keep their current value.
|
|
34
|
+
*
|
|
35
|
+
* @throws RangeError when `patch.intervalMs` is outside
|
|
36
|
+
* [MIN_INTERVAL_MS, MAX_INTERVAL_MS]; filesystem errors propagate.
|
|
37
|
+
*/
|
|
38
|
+
export declare function updateControl(directory: string, patch: ControlPatch): Promise<KeepaliveControl>;
|
|
39
|
+
/** Set the runtime on/off override, preserving any interval override. */
|
|
7
40
|
export declare function writeControl(enabled: boolean, directory: string): Promise<KeepaliveControl>;
|
|
8
41
|
//# sourceMappingURL=control.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../../lib/control.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../../lib/control.ts"],"names":[],"mappings":"AAMA,6FAA6F;AAC7F,eAAO,MAAM,eAAe,QAAS,CAAA;AACrC,4FAA4F;AAC5F,eAAO,MAAM,eAAe,QAAiB,CAAA;AAE7C;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC3B,OAAO,EAAE,CAAC,CAAA;IACV,+BAA+B;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,oFAAoF;AACpF,MAAM,MAAM,YAAY,GAAG;IACvB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B,CAAA;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,OAAO,GAAG,EAAE,IAAI,MAAM,CAOzD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAiB3E;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAC/B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,YAAY,GACpB,OAAO,CAAC,gBAAgB,CAAC,CAoB3B;AAuBD,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAE3F"}
|