@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/dist/lib/keepalive.d.ts
CHANGED
|
@@ -31,7 +31,10 @@ export declare class KeepaliveEngine {
|
|
|
31
31
|
private controlUpdatedAt;
|
|
32
32
|
private reconcilingStatus;
|
|
33
33
|
private enabled;
|
|
34
|
+
private intervalOverride;
|
|
34
35
|
constructor(client: any, config: KeepaliveConfig, store: KeepaliveStore, logger: Logger, directory: string);
|
|
36
|
+
/** Effective ping interval: the runtime override from the control file, else config. */
|
|
37
|
+
private get intervalMs();
|
|
35
38
|
start(): void;
|
|
36
39
|
stop(): void;
|
|
37
40
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keepalive.d.ts","sourceRoot":"","sources":["../../lib/keepalive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAG/C,OAAO,KAAK,EAAgB,cAAc,EAAiC,MAAM,SAAS,CAAA;AAC1F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAgBtC;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,eAAe;
|
|
1
|
+
{"version":3,"file":"keepalive.d.ts","sourceRoot":"","sources":["../../lib/keepalive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAG/C,OAAO,KAAK,EAAgB,cAAc,EAAiC,MAAM,SAAS,CAAA;AAC1F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAgBtC;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,eAAe;IAUpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAb9B,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,WAAW,CAA4C;IAC/D,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,iBAAiB,CAAQ;IACjC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,gBAAgB,CAAoB;gBAGvB,MAAM,EAAE,GAAG,EACX,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM;IAUtC,wFAAwF;IACxF,OAAO,KAAK,UAAU,GAErB;IAED,KAAK,IAAI,IAAI;IAab,IAAI,IAAI,IAAI;IAUZ;;;;OAIG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAKtC,OAAO,CAAC,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAiGxC;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAcvB;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAcvB,0FAA0F;IAC1F,OAAO,CAAC,aAAa;YAWP,SAAS;IAuCvB,uFAAuF;IACvF,OAAO,CAAC,UAAU;IASlB,yFAAyF;IACzF,OAAO,CAAC,cAAc;YAKR,IAAI;YAgCJ,QAAQ;IAsGtB,qEAAqE;YACvD,cAAc;IAW5B,qFAAqF;YACvE,aAAa;YAeb,cAAc;IAc5B,OAAO,CAAC,aAAa;IASrB;;;;OAIG;YACW,UAAU;IAoCxB,OAAO,CAAC,YAAY;YAMN,cAAc;IAiB5B,OAAO,CAAC,WAAW;IA6CnB,sFAAsF;YACxE,eAAe;CAmBhC"}
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -103,8 +103,11 @@ export declare class KeepaliveStore {
|
|
|
103
103
|
private readonly sessions;
|
|
104
104
|
private writeQueue;
|
|
105
105
|
private enabled;
|
|
106
|
+
private intervalMs;
|
|
106
107
|
constructor(config: KeepaliveConfig, directory: string);
|
|
107
108
|
setEnabled(enabled: boolean): void;
|
|
109
|
+
/** Effective ping interval (runtime override or config) reported to the TUI. */
|
|
110
|
+
setIntervalMs(intervalMs: number): void;
|
|
108
111
|
get(sessionID: string): SessionKeepalive | undefined;
|
|
109
112
|
ensure(sessionID: string): SessionKeepalive;
|
|
110
113
|
remove(sessionID: string): void;
|
package/dist/lib/state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../lib/state.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../lib/state.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
1
2
|
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
3
|
+
/**
|
|
4
|
+
* Register the keepalive commands. The control file is scoped per directory, so
|
|
5
|
+
* toggling keepalive or changing its interval here only affects this project's
|
|
6
|
+
* server plugin — not other open opencode sessions. The server polls the file
|
|
7
|
+
* every second, so changes apply without a restart.
|
|
8
|
+
*/
|
|
2
9
|
export declare function registerKeepaliveCommands(api: TuiPluginApi): void;
|
|
3
10
|
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../lib/tui/commands.
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../lib/tui/commands.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC,OAAO,KAAK,EAAyB,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAqBlF;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CA0IjE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../lib/tui/footer.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AAUtC,KAAK,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../lib/tui/footer.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AAUtC,KAAK,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAYhC;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,OAwI7F"}
|
package/dist/lib/tui/format.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
export declare function mmss(ms: number): string;
|
|
2
2
|
export declare function kfmt(n: number): string;
|
|
3
|
+
/**
|
|
4
|
+
* Parse a human duration such as "4m30s", "4.5m", "270s", "1h" or "28m 20s".
|
|
5
|
+
* A bare number means minutes. Returns undefined for anything unparseable;
|
|
6
|
+
* range checks are the caller's concern.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseDuration(input: string): number | undefined;
|
|
9
|
+
/** Compact duration label, e.g. 270000 -> "4m 30s", 3600000 -> "1h". */
|
|
10
|
+
export declare function formatDuration(ms: number): string;
|
|
3
11
|
//# sourceMappingURL=format.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../../lib/tui/format.ts"],"names":[],"mappings":"AAAA,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAKvC;AAED,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAKtC"}
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../../lib/tui/format.ts"],"names":[],"mappings":"AAAA,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAKvC;AAED,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAKtC;AAID;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAa/D;AAED,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAWjD"}
|
package/lib/control.ts
CHANGED
|
@@ -1,37 +1,122 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
1
2
|
import { readFileSync } from "node:fs"
|
|
2
3
|
import { mkdir, rename, writeFile } from "node:fs/promises"
|
|
3
4
|
import { dirname } from "node:path"
|
|
4
5
|
import { controlFilePath } from "./paths"
|
|
5
6
|
|
|
7
|
+
/** Smallest accepted runtime ping interval; below this the 15 s scheduler tick dominates. */
|
|
8
|
+
export const MIN_INTERVAL_MS = 60_000
|
|
9
|
+
/** Largest accepted runtime ping interval (sanity bound; no provider cache outlives it). */
|
|
10
|
+
export const MAX_INTERVAL_MS = 24 * 3_600_000
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Per-project runtime overrides written by the TUI and polled by the server plugin.
|
|
14
|
+
* Absent fields mean "use the plugin configuration". Readers ignore unknown fields,
|
|
15
|
+
* so older servers still honour `enabled` and ignore `intervalMs`.
|
|
16
|
+
*/
|
|
6
17
|
export type KeepaliveControl = {
|
|
7
18
|
version: 1
|
|
8
|
-
|
|
19
|
+
/** Runtime on/off override. */
|
|
20
|
+
enabled?: boolean
|
|
21
|
+
/** Runtime ping interval override in milliseconds. */
|
|
22
|
+
intervalMs?: number
|
|
23
|
+
/** Epoch ms of the last write; strictly increasing so every write is observed. */
|
|
9
24
|
updatedAt: number
|
|
10
25
|
}
|
|
11
26
|
|
|
27
|
+
/** Changes to apply to the control file; `intervalMs: null` clears the override. */
|
|
28
|
+
export type ControlPatch = {
|
|
29
|
+
enabled?: boolean
|
|
30
|
+
intervalMs?: number | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isValidInterval(ms: unknown): ms is number {
|
|
34
|
+
return (
|
|
35
|
+
typeof ms === "number" &&
|
|
36
|
+
Number.isFinite(ms) &&
|
|
37
|
+
ms >= MIN_INTERVAL_MS &&
|
|
38
|
+
ms <= MAX_INTERVAL_MS
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read the directory's control file. Returns undefined when it is missing or
|
|
44
|
+
* malformed; an out-of-range interval override is dropped rather than rejecting
|
|
45
|
+
* the whole file. Never throws.
|
|
46
|
+
*/
|
|
12
47
|
export function readControl(directory: string): KeepaliveControl | undefined {
|
|
13
48
|
try {
|
|
14
|
-
const value = JSON.parse(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (typeof value.updatedAt !== "number") return undefined
|
|
19
|
-
|
|
49
|
+
const value = JSON.parse(readFileSync(controlFilePath(directory), "utf8")) as Record<
|
|
50
|
+
string,
|
|
51
|
+
unknown
|
|
52
|
+
>
|
|
53
|
+
if (value?.version !== 1 || typeof value.updatedAt !== "number") return undefined
|
|
54
|
+
if (value.enabled !== undefined && typeof value.enabled !== "boolean") return undefined
|
|
55
|
+
return {
|
|
56
|
+
version: 1,
|
|
57
|
+
updatedAt: value.updatedAt,
|
|
58
|
+
...(typeof value.enabled === "boolean" ? { enabled: value.enabled } : {}),
|
|
59
|
+
...(isValidInterval(value.intervalMs) ? { intervalMs: value.intervalMs } : {}),
|
|
60
|
+
}
|
|
20
61
|
} catch {
|
|
21
62
|
return undefined
|
|
22
63
|
}
|
|
23
64
|
}
|
|
24
65
|
|
|
25
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Merge `patch` into the directory's control file and write it atomically. Fields
|
|
68
|
+
* not in the patch keep their current value.
|
|
69
|
+
*
|
|
70
|
+
* @throws RangeError when `patch.intervalMs` is outside
|
|
71
|
+
* [MIN_INTERVAL_MS, MAX_INTERVAL_MS]; filesystem errors propagate.
|
|
72
|
+
*/
|
|
73
|
+
export async function updateControl(
|
|
74
|
+
directory: string,
|
|
75
|
+
patch: ControlPatch,
|
|
76
|
+
): Promise<KeepaliveControl> {
|
|
77
|
+
if (
|
|
78
|
+
patch.intervalMs !== undefined &&
|
|
79
|
+
patch.intervalMs !== null &&
|
|
80
|
+
!isValidInterval(patch.intervalMs)
|
|
81
|
+
) {
|
|
82
|
+
throw new RangeError(
|
|
83
|
+
`Ping interval must be between ${MIN_INTERVAL_MS / 1000}s and ${MAX_INTERVAL_MS / 3_600_000}h`,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Serialize read-merge-write within this process so concurrent updates cannot
|
|
88
|
+
// drop each other's fields. Across processes the atomic rename keeps the file
|
|
89
|
+
// valid; the last writer wins.
|
|
90
|
+
const run = updateQueue.then(() => writeMerged(directory, patch))
|
|
91
|
+
updateQueue = run.then(
|
|
92
|
+
() => undefined,
|
|
93
|
+
() => undefined,
|
|
94
|
+
)
|
|
95
|
+
return run
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let updateQueue: Promise<void> = Promise.resolve()
|
|
99
|
+
|
|
100
|
+
async function writeMerged(directory: string, patch: ControlPatch): Promise<KeepaliveControl> {
|
|
101
|
+
const current = readControl(directory)
|
|
102
|
+
const enabled = patch.enabled ?? current?.enabled
|
|
103
|
+
const intervalMs =
|
|
104
|
+
patch.intervalMs === null ? undefined : (patch.intervalMs ?? current?.intervalMs)
|
|
26
105
|
const value: KeepaliveControl = {
|
|
27
106
|
version: 1,
|
|
28
|
-
enabled,
|
|
29
|
-
|
|
107
|
+
...(enabled !== undefined ? { enabled } : {}),
|
|
108
|
+
...(intervalMs !== undefined ? { intervalMs } : {}),
|
|
109
|
+
updatedAt: Math.max(Date.now(), (current?.updatedAt ?? 0) + 1),
|
|
30
110
|
}
|
|
31
111
|
const path = controlFilePath(directory)
|
|
32
|
-
const tmp = `${path}.${process.pid}.${
|
|
112
|
+
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`
|
|
33
113
|
await mkdir(dirname(path), { recursive: true })
|
|
34
114
|
await writeFile(tmp, `${JSON.stringify(value)}\n`, "utf8")
|
|
35
115
|
await rename(tmp, path)
|
|
36
116
|
return value
|
|
37
117
|
}
|
|
118
|
+
|
|
119
|
+
/** Set the runtime on/off override, preserving any interval override. */
|
|
120
|
+
export function writeControl(enabled: boolean, directory: string): Promise<KeepaliveControl> {
|
|
121
|
+
return updateControl(directory, { enabled })
|
|
122
|
+
}
|
package/lib/keepalive.ts
CHANGED
|
@@ -43,6 +43,7 @@ export class KeepaliveEngine {
|
|
|
43
43
|
private controlUpdatedAt = 0
|
|
44
44
|
private reconcilingStatus = false
|
|
45
45
|
private enabled: boolean
|
|
46
|
+
private intervalOverride: number | undefined
|
|
46
47
|
|
|
47
48
|
constructor(
|
|
48
49
|
private readonly client: any,
|
|
@@ -53,8 +54,15 @@ export class KeepaliveEngine {
|
|
|
53
54
|
) {
|
|
54
55
|
const control = readControl(directory)
|
|
55
56
|
this.enabled = control?.enabled ?? config.enabled
|
|
57
|
+
this.intervalOverride = control?.intervalMs
|
|
56
58
|
this.controlUpdatedAt = control?.updatedAt ?? 0
|
|
57
59
|
this.store.setEnabled(this.enabled)
|
|
60
|
+
this.store.setIntervalMs(this.intervalMs)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Effective ping interval: the runtime override from the control file, else config. */
|
|
64
|
+
private get intervalMs(): number {
|
|
65
|
+
return this.intervalOverride ?? this.config.intervalMs
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
start(): void {
|
|
@@ -65,7 +73,7 @@ export class KeepaliveEngine {
|
|
|
65
73
|
this.store.persist()
|
|
66
74
|
this.logger.info(
|
|
67
75
|
`started (${this.enabled ? "enabled" : "disabled"}, ` +
|
|
68
|
-
`interval=${Math.round(this.
|
|
76
|
+
`interval=${Math.round(this.intervalMs / 1000)}s, ` +
|
|
69
77
|
`window=${Math.round(this.config.windowMs / 60_000)}m)`,
|
|
70
78
|
)
|
|
71
79
|
}
|
|
@@ -282,7 +290,7 @@ export class KeepaliveEngine {
|
|
|
282
290
|
private openWindow(s: SessionKeepalive, now: number): void {
|
|
283
291
|
s.idleSince = now
|
|
284
292
|
s.windowEndsAt = s.lastResponseAt + this.config.windowMs
|
|
285
|
-
s.nextPingAt = Math.max(now, lastCacheTouch(s) + this.
|
|
293
|
+
s.nextPingAt = Math.max(now, lastCacheTouch(s) + this.intervalMs)
|
|
286
294
|
s.pingsSent = 0
|
|
287
295
|
s.lastPing = undefined
|
|
288
296
|
s.active = true
|
|
@@ -291,7 +299,7 @@ export class KeepaliveEngine {
|
|
|
291
299
|
/** False once the cache has gone too long without a request to still be trusted warm. */
|
|
292
300
|
private cacheMayBeWarm(s: SessionKeepalive, now: number): boolean {
|
|
293
301
|
const touch = lastCacheTouch(s)
|
|
294
|
-
return touch > 0 && now - touch <= COLD_AFTER_INTERVALS * this.
|
|
302
|
+
return touch > 0 && now - touch <= COLD_AFTER_INTERVALS * this.intervalMs
|
|
295
303
|
}
|
|
296
304
|
|
|
297
305
|
private async tick(): Promise<void> {
|
|
@@ -415,8 +423,8 @@ export class KeepaliveEngine {
|
|
|
415
423
|
// Schedule relative to the intended time so ping duration doesn't
|
|
416
424
|
// compound drift, but never into the past: that would fire catch-up
|
|
417
425
|
// pings on consecutive ticks.
|
|
418
|
-
const next = s.nextPingAt + jitter(this.
|
|
419
|
-
s.nextPingAt = next > now ? next : now + jitter(this.
|
|
426
|
+
const next = s.nextPingAt + jitter(this.intervalMs)
|
|
427
|
+
s.nextPingAt = next > now ? next : now + jitter(this.intervalMs)
|
|
420
428
|
}
|
|
421
429
|
this.store.persist()
|
|
422
430
|
if (ping.sent && ping.interrupted) {
|
|
@@ -547,24 +555,45 @@ export class KeepaliveEngine {
|
|
|
547
555
|
const control = readControl(this.directory)
|
|
548
556
|
if (!control || control.updatedAt <= this.controlUpdatedAt) return
|
|
549
557
|
this.controlUpdatedAt = control.updatedAt
|
|
550
|
-
if (control.enabled === this.enabled) return
|
|
551
|
-
|
|
552
|
-
this.enabled = control.enabled
|
|
553
|
-
this.store.setEnabled(control.enabled)
|
|
554
558
|
const now = Date.now()
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
+
let changed = false
|
|
560
|
+
|
|
561
|
+
if (control.intervalMs !== this.intervalOverride) {
|
|
562
|
+
this.intervalOverride = control.intervalMs
|
|
563
|
+
this.store.setIntervalMs(this.intervalMs)
|
|
564
|
+
// Re-anchor pending pings on the new cadence. An in-flight ping schedules
|
|
565
|
+
// its successor with the new interval when it finishes.
|
|
566
|
+
for (const session of this.store.all()) {
|
|
567
|
+
if (session.active && !session.warming)
|
|
568
|
+
session.nextPingAt = Math.max(now, lastCacheTouch(session) + this.intervalMs)
|
|
569
|
+
}
|
|
570
|
+
this.logger.info(
|
|
571
|
+
`runtime interval ${Math.round(this.intervalMs / 1000)}s` +
|
|
572
|
+
(control.intervalMs === undefined ? " (config)" : ""),
|
|
573
|
+
)
|
|
574
|
+
changed = true
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const enabled = control.enabled ?? this.config.enabled
|
|
578
|
+
if (enabled !== this.enabled) {
|
|
579
|
+
this.enabled = enabled
|
|
580
|
+
this.store.setEnabled(enabled)
|
|
581
|
+
for (const session of this.store.all()) {
|
|
582
|
+
if (!enabled) {
|
|
583
|
+
session.active = false
|
|
584
|
+
continue
|
|
585
|
+
}
|
|
586
|
+
// Only re-arm sessions with a real response whose cache may still be warm,
|
|
587
|
+
// and that are not currently occupied.
|
|
588
|
+
if (!session.eligible || session.busy || session.warming) continue
|
|
589
|
+
if (!session.lastResponseAt || !this.cacheMayBeWarm(session, now)) continue
|
|
590
|
+
this.openWindow(session, now)
|
|
559
591
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (!session.eligible || session.busy || session.warming) continue
|
|
563
|
-
if (!session.lastResponseAt || !this.cacheMayBeWarm(session, now)) continue
|
|
564
|
-
this.openWindow(session, now)
|
|
592
|
+
this.logger.info(`runtime ${enabled ? "enabled" : "disabled"}`)
|
|
593
|
+
changed = true
|
|
565
594
|
}
|
|
566
|
-
|
|
567
|
-
this.
|
|
595
|
+
|
|
596
|
+
if (changed) this.store.persist()
|
|
568
597
|
}
|
|
569
598
|
|
|
570
599
|
/** Recover when a session.status idle event is missed by reconciling with the API. */
|
package/lib/state.ts
CHANGED
|
@@ -111,18 +111,25 @@ export class KeepaliveStore {
|
|
|
111
111
|
private readonly sessions = new Map<string, SessionKeepalive>()
|
|
112
112
|
private writeQueue: Promise<void> = Promise.resolve()
|
|
113
113
|
private enabled: boolean
|
|
114
|
+
private intervalMs: number
|
|
114
115
|
|
|
115
116
|
constructor(
|
|
116
117
|
private readonly config: KeepaliveConfig,
|
|
117
118
|
private readonly directory: string,
|
|
118
119
|
) {
|
|
119
120
|
this.enabled = config.enabled
|
|
121
|
+
this.intervalMs = config.intervalMs
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
setEnabled(enabled: boolean): void {
|
|
123
125
|
this.enabled = enabled
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
/** Effective ping interval (runtime override or config) reported to the TUI. */
|
|
129
|
+
setIntervalMs(intervalMs: number): void {
|
|
130
|
+
this.intervalMs = intervalMs
|
|
131
|
+
}
|
|
132
|
+
|
|
126
133
|
get(sessionID: string): SessionKeepalive | undefined {
|
|
127
134
|
return this.sessions.get(sessionID)
|
|
128
135
|
}
|
|
@@ -163,7 +170,7 @@ export class KeepaliveStore {
|
|
|
163
170
|
version: 1,
|
|
164
171
|
updatedAt: Date.now(),
|
|
165
172
|
enabled: this.enabled,
|
|
166
|
-
intervalMs: this.
|
|
173
|
+
intervalMs: this.intervalMs,
|
|
167
174
|
windowMs: this.config.windowMs,
|
|
168
175
|
sessions: Object.fromEntries(
|
|
169
176
|
this.all().map((s) => [
|
|
@@ -175,7 +182,7 @@ export class KeepaliveStore {
|
|
|
175
182
|
idleSince: s.idleSince,
|
|
176
183
|
windowEndsAt: s.windowEndsAt,
|
|
177
184
|
nextPingAt: s.nextPingAt,
|
|
178
|
-
intervalMs: this.
|
|
185
|
+
intervalMs: this.intervalMs,
|
|
179
186
|
pingsSent: s.pingsSent,
|
|
180
187
|
busy: s.busy,
|
|
181
188
|
warming: s.warming,
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import type { TuiDialogSelectOption, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
4
|
+
import { readFileSync } from "node:fs"
|
|
5
|
+
import {
|
|
6
|
+
type ControlPatch,
|
|
7
|
+
isValidInterval,
|
|
8
|
+
MAX_INTERVAL_MS,
|
|
9
|
+
MIN_INTERVAL_MS,
|
|
10
|
+
readControl,
|
|
11
|
+
updateControl,
|
|
12
|
+
} from "../control"
|
|
13
|
+
import { stateFilePath } from "../paths"
|
|
14
|
+
import { formatDuration, parseDuration } from "./format"
|
|
15
|
+
|
|
16
|
+
/** Interval presets matched to common provider prompt-cache lifetimes. */
|
|
17
|
+
const INTERVAL_PRESETS: { ms: number; description: string }[] = [
|
|
18
|
+
{ ms: 270_000, description: "5-minute caches (Claude / Anthropic)" },
|
|
19
|
+
{ ms: 1_700_000, description: "30-minute caches (GPT / OpenAI)" },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
type IntervalChoice = number | "config" | "custom"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Register the keepalive commands. The control file is scoped per directory, so
|
|
26
|
+
* toggling keepalive or changing its interval here only affects this project's
|
|
27
|
+
* server plugin — not other open opencode sessions. The server polls the file
|
|
28
|
+
* every second, so changes apply without a restart.
|
|
29
|
+
*/
|
|
30
|
+
export function registerKeepaliveCommands(api: TuiPluginApi): void {
|
|
31
|
+
// Read lazily: state paths may not be populated at registration time.
|
|
32
|
+
const directory = () => api.state.path.directory
|
|
33
|
+
|
|
34
|
+
const apply = async (
|
|
35
|
+
patch: ControlPatch,
|
|
36
|
+
message: string,
|
|
37
|
+
variant: "success" | "warning" | "info",
|
|
38
|
+
) => {
|
|
39
|
+
try {
|
|
40
|
+
await updateControl(directory(), patch)
|
|
41
|
+
api.ui.toast({ variant, title: "Cache keepalive", message })
|
|
42
|
+
} catch (error) {
|
|
43
|
+
api.ui.toast({
|
|
44
|
+
variant: "error",
|
|
45
|
+
title: "Cache keepalive",
|
|
46
|
+
message: error instanceof Error ? error.message : String(error),
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const setEnabled = (enabled: boolean) =>
|
|
52
|
+
apply({ enabled }, enabled ? "Enabled" : "Disabled", enabled ? "success" : "warning")
|
|
53
|
+
|
|
54
|
+
const setPingInterval = (intervalMs: number | null) =>
|
|
55
|
+
apply(
|
|
56
|
+
{ intervalMs },
|
|
57
|
+
intervalMs === null
|
|
58
|
+
? "Ping interval reset to the plugin default"
|
|
59
|
+
: `Ping interval ${formatDuration(intervalMs)}`,
|
|
60
|
+
"info",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const showCustomInterval = (current: number | undefined) => {
|
|
64
|
+
api.ui.dialog.replace(() => (
|
|
65
|
+
<api.ui.DialogPrompt
|
|
66
|
+
title="Custom ping interval"
|
|
67
|
+
placeholder="e.g. 4m30s, 270s, 12m (a bare number means minutes)"
|
|
68
|
+
value={current ? formatDuration(current).replace(/ /g, "") : undefined}
|
|
69
|
+
onConfirm={(raw) => {
|
|
70
|
+
const ms = parseDuration(raw)
|
|
71
|
+
if (ms === undefined || !isValidInterval(ms)) {
|
|
72
|
+
api.ui.toast({
|
|
73
|
+
variant: "error",
|
|
74
|
+
title: "Cache keepalive",
|
|
75
|
+
message: `Enter a duration between ${formatDuration(MIN_INTERVAL_MS)} and ${formatDuration(MAX_INTERVAL_MS)}, e.g. 4m30s`,
|
|
76
|
+
})
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
api.ui.dialog.clear()
|
|
80
|
+
void setPingInterval(ms)
|
|
81
|
+
}}
|
|
82
|
+
onCancel={() => api.ui.dialog.clear()}
|
|
83
|
+
/>
|
|
84
|
+
))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const showInterval = () => {
|
|
88
|
+
const override = readControl(directory())?.intervalMs
|
|
89
|
+
const options: TuiDialogSelectOption<IntervalChoice>[] = INTERVAL_PRESETS.map((preset) => ({
|
|
90
|
+
title: formatDuration(preset.ms),
|
|
91
|
+
value: preset.ms,
|
|
92
|
+
description: preset.description,
|
|
93
|
+
}))
|
|
94
|
+
if (override !== undefined && !INTERVAL_PRESETS.some((preset) => preset.ms === override)) {
|
|
95
|
+
options.push({
|
|
96
|
+
title: formatDuration(override),
|
|
97
|
+
value: override,
|
|
98
|
+
description: "Current custom interval",
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
options.push(
|
|
102
|
+
{
|
|
103
|
+
title: "Custom…",
|
|
104
|
+
value: "custom",
|
|
105
|
+
description: "Enter a duration, e.g. 4m30s or 12m",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
title: "Plugin default",
|
|
109
|
+
value: "config",
|
|
110
|
+
description: "Clear the override and use the configured intervalMs",
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
api.ui.dialog.replace(() => (
|
|
115
|
+
<api.ui.DialogSelect
|
|
116
|
+
title="Keepalive ping interval"
|
|
117
|
+
options={options}
|
|
118
|
+
current={override ?? "config"}
|
|
119
|
+
onSelect={(option) => {
|
|
120
|
+
const choice = option.value as IntervalChoice
|
|
121
|
+
if (choice === "custom") {
|
|
122
|
+
showCustomInterval(override)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
api.ui.dialog.clear()
|
|
126
|
+
void setPingInterval(choice === "config" ? null : choice)
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
api.keymap.registerLayer({
|
|
133
|
+
commands: [
|
|
134
|
+
{
|
|
135
|
+
name: "keepalive.toggle",
|
|
136
|
+
title: "Toggle cache keepalive",
|
|
137
|
+
category: "Keepalive",
|
|
138
|
+
namespace: "palette",
|
|
139
|
+
slashName: "keepalive-toggle",
|
|
140
|
+
run: () => setEnabled(!currentEnabled(directory())),
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "keepalive.on",
|
|
144
|
+
title: "Enable cache keepalive",
|
|
145
|
+
category: "Keepalive",
|
|
146
|
+
namespace: "palette",
|
|
147
|
+
slashName: "keepalive-on",
|
|
148
|
+
run: () => setEnabled(true),
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "keepalive.off",
|
|
152
|
+
title: "Disable cache keepalive",
|
|
153
|
+
category: "Keepalive",
|
|
154
|
+
namespace: "palette",
|
|
155
|
+
slashName: "keepalive-off",
|
|
156
|
+
run: () => setEnabled(false),
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "keepalive.interval",
|
|
160
|
+
title: "Set cache keepalive ping interval",
|
|
161
|
+
category: "Keepalive",
|
|
162
|
+
namespace: "palette",
|
|
163
|
+
slashName: "keepalive-interval",
|
|
164
|
+
run: showInterval,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Effective on/off state: the runtime override if set, else what this project's
|
|
172
|
+
* server last reported (its configured default), else on.
|
|
173
|
+
*/
|
|
174
|
+
function currentEnabled(directory: string): boolean {
|
|
175
|
+
const override = readControl(directory)?.enabled
|
|
176
|
+
if (override !== undefined) return override
|
|
177
|
+
try {
|
|
178
|
+
const state = JSON.parse(readFileSync(stateFilePath(directory), "utf8"))
|
|
179
|
+
if (typeof state?.enabled === "boolean") return state.enabled
|
|
180
|
+
} catch {
|
|
181
|
+
// No server snapshot yet.
|
|
182
|
+
}
|
|
183
|
+
return true
|
|
184
|
+
}
|
package/lib/tui/footer.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { readdirSync, readFileSync } from "node:fs"
|
|
|
6
6
|
import { join } from "node:path"
|
|
7
7
|
import { stateDirectoryPath, stateFilePath } from "../paths"
|
|
8
8
|
import type { PersistedSession, PersistedState, PingRecord } from "../state"
|
|
9
|
-
import { kfmt, mmss } from "./format"
|
|
9
|
+
import { formatDuration, kfmt, mmss } from "./format"
|
|
10
10
|
|
|
11
11
|
type Theme = Record<string, any>
|
|
12
12
|
type Status = "waiting" | "off" | "warming" | "busy" | "armed" | "cold"
|
|
@@ -16,6 +16,8 @@ type Derived = {
|
|
|
16
16
|
left: number
|
|
17
17
|
status: Status
|
|
18
18
|
lastPing?: PingRecord
|
|
19
|
+
/** Effective ping interval for this project, when the server has reported it. */
|
|
20
|
+
intervalMs?: number
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -60,12 +62,25 @@ export function KeepaliveFooter(props: { theme: Theme; directory: string; sessio
|
|
|
60
62
|
const snapshot = state()
|
|
61
63
|
const sessionID = props.sessionID
|
|
62
64
|
if (!snapshot || !sessionID) {
|
|
63
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
idleMs: 0,
|
|
67
|
+
sent: 0,
|
|
68
|
+
left: 0,
|
|
69
|
+
status: "waiting",
|
|
70
|
+
intervalMs: snapshot?.intervalMs,
|
|
71
|
+
}
|
|
64
72
|
}
|
|
65
73
|
const session: PersistedSession | undefined = snapshot.sessions?.[sessionID]
|
|
66
|
-
if (!session)
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
if (!session)
|
|
75
|
+
return {
|
|
76
|
+
idleMs: 0,
|
|
77
|
+
sent: 0,
|
|
78
|
+
left: 0,
|
|
79
|
+
status: "waiting",
|
|
80
|
+
intervalMs: snapshot.intervalMs,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const interval = snapshot.intervalMs || session.intervalMs || 270_000
|
|
69
84
|
const idleMs = Math.max(0, now() - (session.lastResponseAt || now()))
|
|
70
85
|
// `nextPingAt` is the scheduler's source of truth and includes the
|
|
71
86
|
// per-ping jitter. Counting from it keeps the two counters in step.
|
|
@@ -84,7 +99,14 @@ export function KeepaliveFooter(props: { theme: Theme; directory: string; sessio
|
|
|
84
99
|
? "armed"
|
|
85
100
|
: "cold"
|
|
86
101
|
|
|
87
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
idleMs,
|
|
104
|
+
sent: session.pingsSent || 0,
|
|
105
|
+
left,
|
|
106
|
+
status,
|
|
107
|
+
lastPing: session.lastPing,
|
|
108
|
+
intervalMs: interval,
|
|
109
|
+
}
|
|
88
110
|
}
|
|
89
111
|
|
|
90
112
|
const theme = props.theme
|
|
@@ -100,6 +122,11 @@ export function KeepaliveFooter(props: { theme: Theme; directory: string; sessio
|
|
|
100
122
|
<text fg={statusColor(theme, data().status)}>
|
|
101
123
|
{statusLabel(data().status)}
|
|
102
124
|
</text>
|
|
125
|
+
<Show when={data().intervalMs}>
|
|
126
|
+
{(interval: () => number) => (
|
|
127
|
+
<text fg={theme.textMuted}>every {formatDuration(interval())}</text>
|
|
128
|
+
)}
|
|
129
|
+
</Show>
|
|
103
130
|
</box>
|
|
104
131
|
|
|
105
132
|
<box flexDirection="row" gap={1}>
|