@vikrant82/opencode-cache-keepalive 0.1.0 → 0.1.2
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/LICENSE +661 -0
- package/README.md +133 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +76 -31
- package/dist/index.js.map +1 -1
- package/dist/lib/keepalive.d.ts.map +1 -1
- package/dist/lib/logger.d.ts +4 -1
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/paths.d.ts +2 -0
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/state.d.ts +5 -1
- package/dist/lib/state.d.ts.map +1 -1
- package/dist/lib/tui/footer.d.ts.map +1 -1
- package/lib/keepalive.ts +68 -24
- package/lib/logger.ts +31 -5
- package/lib/paths.ts +5 -0
- package/lib/state.ts +5 -1
- package/lib/tui/footer.tsx +9 -2
- package/package.json +1 -1
package/dist/lib/state.d.ts
CHANGED
|
@@ -21,14 +21,18 @@ export type SessionKeepalive = {
|
|
|
21
21
|
windowEndsAt: number;
|
|
22
22
|
/** Epoch ms the next ping is due. */
|
|
23
23
|
nextPingAt: number;
|
|
24
|
-
/**
|
|
24
|
+
/** Number of scheduled ping requests attempted during this warm window. */
|
|
25
25
|
pingsSent: number;
|
|
26
26
|
/** A real turn is currently running. */
|
|
27
27
|
busy: boolean;
|
|
28
|
+
/** Epoch ms when `busy` was last set; used as a stuck-busy timeout backstop. */
|
|
29
|
+
busySince?: number;
|
|
28
30
|
/** A keepalive ping is currently in flight. */
|
|
29
31
|
warming: boolean;
|
|
30
32
|
/** Within the warm window and actively scheduling pings. */
|
|
31
33
|
active: boolean;
|
|
34
|
+
/** True while `armWindow` is resolving session metadata (async guard). */
|
|
35
|
+
arming?: boolean;
|
|
32
36
|
/** Epoch ms of the last ping; retained only to ignore its late events. */
|
|
33
37
|
lastPingAt?: number;
|
|
34
38
|
lastPing?: PingRecord;
|
package/dist/lib/state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../lib/state.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAG/C,MAAM,MAAM,UAAU,GAAG;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,OAAO,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,8DAA8D;AAC9D,MAAM,MAAM,gBAAgB,GAAG;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,4EAA4E;IAC5E,cAAc,EAAE,MAAM,CAAA;IACtB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB,8EAA8E;IAC9E,YAAY,EAAE,MAAM,CAAA;IACpB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../lib/state.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAG/C,MAAM,MAAM,UAAU,GAAG;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,OAAO,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,8DAA8D;AAC9D,MAAM,MAAM,gBAAgB,GAAG;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,4EAA4E;IAC5E,cAAc,EAAE,MAAM,CAAA;IACtB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB,8EAA8E;IAC9E,YAAY,EAAE,MAAM,CAAA;IACpB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;IACjB,wCAAwC;IACxC,IAAI,EAAE,OAAO,CAAA;IACb,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAA;IAChB,4DAA4D;IAC5D,MAAM,EAAE,OAAO,CAAA;IACf,0EAA0E;IAC1E,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,UAAU,CAAA;CACxB,CAAA;AAED,4DAA4D;AAC5D,MAAM,MAAM,gBAAgB,GAAG;IAC3B,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,OAAO,CAAA;IACf,QAAQ,CAAC,EAAE,UAAU,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IACzB,OAAO,EAAE,CAAC,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;CAC7C,CAAA;AAED,qBAAa,cAAc;IAMnB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAN9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;IAC/D,OAAO,CAAC,UAAU,CAAmC;IACrD,OAAO,CAAC,OAAO,CAAS;gBAGH,MAAM,EAAE,eAAe,EACvB,SAAS,EAAE,MAAM;IAKtC,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIlC,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;IAIpD,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB;IAmB3C,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI/B,GAAG,IAAI,gBAAgB,EAAE;IAIzB,+EAA+E;IAC/E,OAAO,IAAI,IAAI;CA+BlB"}
|
|
@@ -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;AAUhC;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,
|
|
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;AAUhC;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,OA+G7F"}
|
package/lib/keepalive.ts
CHANGED
|
@@ -8,6 +8,9 @@ const TICK_MS = 15_000
|
|
|
8
8
|
const JITTER_MS = 15_000
|
|
9
9
|
const CONTROL_POLL_MS = 1_000
|
|
10
10
|
const STATUS_POLL_MS = 2_000
|
|
11
|
+
const PING_STATUS_GRACE_MS = 5_000
|
|
12
|
+
/** Backstop: clear a stuck `busy` flag after this long without an idle event. */
|
|
13
|
+
const BUSY_TIMEOUT_MS = 300_000
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* The keepalive engine.
|
|
@@ -97,6 +100,7 @@ export class KeepaliveEngine {
|
|
|
97
100
|
if (info?.role === "user") {
|
|
98
101
|
const s = this.store.ensure(sessionID)
|
|
99
102
|
s.busy = true
|
|
103
|
+
s.busySince = Date.now()
|
|
100
104
|
s.active = false
|
|
101
105
|
this.store.persist()
|
|
102
106
|
return
|
|
@@ -115,8 +119,17 @@ export class KeepaliveEngine {
|
|
|
115
119
|
const status: string | undefined = event.properties?.status?.type
|
|
116
120
|
if (!sessionID || !status) return
|
|
117
121
|
|
|
118
|
-
// The ping
|
|
119
|
-
|
|
122
|
+
// The ping and its optional revert emit status transitions after their
|
|
123
|
+
// requests resolve. Ignore them briefly so they cannot re-arm/reset the
|
|
124
|
+
// active warm window. Real user-message events still stop warming now.
|
|
125
|
+
const current = this.store.get(sessionID)
|
|
126
|
+
if (
|
|
127
|
+
current?.warming ||
|
|
128
|
+
(status !== "idle" &&
|
|
129
|
+
current?.lastPingAt &&
|
|
130
|
+
Date.now() - current.lastPingAt < PING_STATUS_GRACE_MS)
|
|
131
|
+
)
|
|
132
|
+
return
|
|
120
133
|
|
|
121
134
|
if (status === "idle") {
|
|
122
135
|
await this.armWindow(sessionID)
|
|
@@ -125,6 +138,7 @@ export class KeepaliveEngine {
|
|
|
125
138
|
|
|
126
139
|
const s = this.store.ensure(sessionID)
|
|
127
140
|
s.busy = true
|
|
141
|
+
s.busySince = Date.now()
|
|
128
142
|
s.active = false
|
|
129
143
|
this.store.persist()
|
|
130
144
|
return
|
|
@@ -144,32 +158,52 @@ export class KeepaliveEngine {
|
|
|
144
158
|
// Duplicate idle events (including late events from a ping) must not reset the
|
|
145
159
|
// current idle stretch or extend its warm window.
|
|
146
160
|
if (s.active && !s.busy) return
|
|
147
|
-
|
|
148
|
-
|
|
161
|
+
// Serialize: only one armWindow may resolve metadata at a time per session.
|
|
162
|
+
// This prevents the session.status + session.idle double-event race from
|
|
163
|
+
// producing two concurrent resolveSession calls that can clobber each other.
|
|
164
|
+
if (s.arming) return
|
|
165
|
+
s.arming = true
|
|
166
|
+
try {
|
|
167
|
+
s.busy = false
|
|
168
|
+
s.busySince = undefined
|
|
169
|
+
await this.resolveSession(s)
|
|
149
170
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
171
|
+
// A new turn may have started while session metadata was being resolved.
|
|
172
|
+
if (s.busy || !this.enabled || !s.eligible) {
|
|
173
|
+
s.active = false
|
|
174
|
+
this.store.persist()
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const now = Date.now()
|
|
179
|
+
if (!s.lastResponseAt) s.lastResponseAt = now
|
|
180
|
+
s.idleSince = now
|
|
181
|
+
s.windowEndsAt = now + this.config.windowMs
|
|
182
|
+
s.nextPingAt = now + this.config.intervalMs
|
|
183
|
+
s.pingsSent = 0
|
|
184
|
+
s.lastPing = undefined
|
|
185
|
+
s.active = true
|
|
153
186
|
this.store.persist()
|
|
154
|
-
|
|
187
|
+
this.logger.dbg(`armed ${short(sessionID)} model=${s.modelLabel}`)
|
|
188
|
+
} finally {
|
|
189
|
+
s.arming = false
|
|
155
190
|
}
|
|
156
|
-
|
|
157
|
-
const now = Date.now()
|
|
158
|
-
if (!s.lastResponseAt) s.lastResponseAt = now
|
|
159
|
-
s.idleSince = now
|
|
160
|
-
s.windowEndsAt = now + this.config.windowMs
|
|
161
|
-
s.nextPingAt = now + this.config.intervalMs
|
|
162
|
-
s.pingsSent = 0
|
|
163
|
-
s.lastPing = undefined
|
|
164
|
-
s.active = true
|
|
165
|
-
this.store.persist()
|
|
166
|
-
this.logger.dbg(`armed ${short(sessionID)} model=${s.modelLabel}`)
|
|
167
191
|
}
|
|
168
192
|
|
|
169
193
|
private async tick(): Promise<void> {
|
|
170
194
|
if (!this.enabled) return
|
|
171
195
|
const now = Date.now()
|
|
172
196
|
for (const s of this.store.all()) {
|
|
197
|
+
// Backstop: if busy is stuck (missed idle event + reconcile failure),
|
|
198
|
+
// clear it after BUSY_TIMEOUT_MS so the session can re-arm.
|
|
199
|
+
if (s.busy && s.busySince && now - s.busySince > BUSY_TIMEOUT_MS) {
|
|
200
|
+
this.logger.dbg(`busy timeout ${short(s.sessionID)} — clearing stuck flag`)
|
|
201
|
+
s.busy = false
|
|
202
|
+
s.busySince = undefined
|
|
203
|
+
void this.armWindow(s.sessionID)
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
|
|
173
207
|
if (!s.eligible || !s.active || s.busy || s.warming) continue
|
|
174
208
|
|
|
175
209
|
if (now >= s.windowEndsAt) {
|
|
@@ -190,6 +224,9 @@ export class KeepaliveEngine {
|
|
|
190
224
|
if (!s || s.warming || s.busy) return
|
|
191
225
|
|
|
192
226
|
s.warming = true
|
|
227
|
+
// Count every request attempt. A transport/provider failure still consumed a
|
|
228
|
+
// scheduled ping slot and must remain visible in the footer.
|
|
229
|
+
s.pingsSent += 1
|
|
193
230
|
this.store.persist()
|
|
194
231
|
const startAt = Date.now()
|
|
195
232
|
|
|
@@ -210,9 +247,7 @@ export class KeepaliveEngine {
|
|
|
210
247
|
output: Number(info?.tokens?.output ?? 0),
|
|
211
248
|
}
|
|
212
249
|
s.lastPing = record
|
|
213
|
-
|
|
214
|
-
s.pingsSent += 1
|
|
215
|
-
this.logger.info(
|
|
250
|
+
this.logger.dbg(
|
|
216
251
|
`ping ${short(sessionID)} ${record.hit ? "HIT" : "MISS"} ` +
|
|
217
252
|
`in=${record.input} read=${record.cacheRead} write=${record.cacheWrite} out=${record.output}`,
|
|
218
253
|
)
|
|
@@ -222,7 +257,14 @@ export class KeepaliveEngine {
|
|
|
222
257
|
this.logger.warn(`ping ${short(sessionID)} failed`, errText(err))
|
|
223
258
|
} finally {
|
|
224
259
|
s.warming = false
|
|
225
|
-
s.
|
|
260
|
+
s.lastPingAt = Date.now()
|
|
261
|
+
// Schedule relative to the intended time so ping duration doesn't
|
|
262
|
+
// compound drift. Floor at now+1s to avoid immediate re-fire if the
|
|
263
|
+
// ping took longer than the interval.
|
|
264
|
+
s.nextPingAt = Math.max(
|
|
265
|
+
Date.now() + 1_000,
|
|
266
|
+
s.nextPingAt + jitter(this.config.intervalMs),
|
|
267
|
+
)
|
|
226
268
|
this.store.persist()
|
|
227
269
|
}
|
|
228
270
|
}
|
|
@@ -297,8 +339,10 @@ export class KeepaliveEngine {
|
|
|
297
339
|
session.active = false
|
|
298
340
|
continue
|
|
299
341
|
}
|
|
342
|
+
// Only re-arm sessions that have had a real turn (cache to warm) and
|
|
343
|
+
// are not currently occupied.
|
|
300
344
|
if (!session.eligible || session.busy || session.warming) continue
|
|
301
|
-
if (!session.lastResponseAt)
|
|
345
|
+
if (!session.lastResponseAt) continue
|
|
302
346
|
session.idleSince = now
|
|
303
347
|
session.windowEndsAt = now + this.config.windowMs
|
|
304
348
|
session.nextPingAt = now + this.config.intervalMs
|
package/lib/logger.ts
CHANGED
|
@@ -1,21 +1,47 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs"
|
|
2
|
+
import { dirname } from "node:path"
|
|
3
|
+
|
|
1
4
|
const PREFIX = "[cache-keepalive]"
|
|
2
5
|
|
|
3
6
|
export class Logger {
|
|
4
|
-
|
|
7
|
+
private readonly logPath: string
|
|
8
|
+
private readonly debug: boolean
|
|
9
|
+
private initialized = false
|
|
10
|
+
|
|
11
|
+
constructor(debug: boolean, logPath: string) {
|
|
12
|
+
this.debug = debug
|
|
13
|
+
this.logPath = logPath
|
|
14
|
+
}
|
|
5
15
|
|
|
6
16
|
info(message: string, data?: unknown) {
|
|
7
|
-
|
|
17
|
+
this.write("INFO", message, data)
|
|
8
18
|
}
|
|
9
19
|
|
|
10
20
|
warn(message: string, data?: unknown) {
|
|
11
|
-
|
|
21
|
+
this.write("WARN", message, data)
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
error(message: string, data?: unknown) {
|
|
15
|
-
|
|
25
|
+
this.write("ERROR", message, data)
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
dbg(message: string, data?: unknown) {
|
|
19
|
-
if (this.debug)
|
|
29
|
+
if (this.debug) this.write("DEBUG", message, data)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private write(level: string, message: string, data?: unknown): void {
|
|
33
|
+
const ts = new Date().toISOString()
|
|
34
|
+
const extra =
|
|
35
|
+
data !== undefined ? ` ${typeof data === "string" ? data : JSON.stringify(data)}` : ""
|
|
36
|
+
const line = `${ts} ${level} ${PREFIX} ${message}${extra}\n`
|
|
37
|
+
try {
|
|
38
|
+
if (!this.initialized) {
|
|
39
|
+
mkdirSync(dirname(this.logPath), { recursive: true })
|
|
40
|
+
this.initialized = true
|
|
41
|
+
}
|
|
42
|
+
appendFileSync(this.logPath, line, "utf8")
|
|
43
|
+
} catch {
|
|
44
|
+
// Best-effort logging — never crash the plugin.
|
|
45
|
+
}
|
|
20
46
|
}
|
|
21
47
|
}
|
package/lib/paths.ts
CHANGED
|
@@ -29,3 +29,8 @@ export function stateDirectoryPath(): string {
|
|
|
29
29
|
export function controlFilePath(): string {
|
|
30
30
|
return join(stateDirectoryPath(), "control.json")
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
/** Append-only log file for the server plugin — never stdout, which corrupts the TUI. */
|
|
34
|
+
export function logFilePath(): string {
|
|
35
|
+
return join(stateDirectoryPath(), "server.log")
|
|
36
|
+
}
|
package/lib/state.ts
CHANGED
|
@@ -26,14 +26,18 @@ export type SessionKeepalive = {
|
|
|
26
26
|
windowEndsAt: number
|
|
27
27
|
/** Epoch ms the next ping is due. */
|
|
28
28
|
nextPingAt: number
|
|
29
|
-
/**
|
|
29
|
+
/** Number of scheduled ping requests attempted during this warm window. */
|
|
30
30
|
pingsSent: number
|
|
31
31
|
/** A real turn is currently running. */
|
|
32
32
|
busy: boolean
|
|
33
|
+
/** Epoch ms when `busy` was last set; used as a stuck-busy timeout backstop. */
|
|
34
|
+
busySince?: number
|
|
33
35
|
/** A keepalive ping is currently in flight. */
|
|
34
36
|
warming: boolean
|
|
35
37
|
/** Within the warm window and actively scheduling pings. */
|
|
36
38
|
active: boolean
|
|
39
|
+
/** True while `armWindow` is resolving session metadata (async guard). */
|
|
40
|
+
arming?: boolean
|
|
37
41
|
/** Epoch ms of the last ping; retained only to ignore its late events. */
|
|
38
42
|
lastPingAt?: number
|
|
39
43
|
lastPing?: PingRecord
|
package/lib/tui/footer.tsx
CHANGED
|
@@ -38,7 +38,11 @@ export function KeepaliveFooter(props: { theme: Theme; directory: string; sessio
|
|
|
38
38
|
const match = readdirSync(stateDirectoryPath())
|
|
39
39
|
.filter((name) => name.startsWith("state-") && name.endsWith(".json"))
|
|
40
40
|
.map((name) => readState(join(stateDirectoryPath(), name)))
|
|
41
|
-
.
|
|
41
|
+
.filter(
|
|
42
|
+
(snapshot): snapshot is PersistedState =>
|
|
43
|
+
snapshot?.sessions?.[sessionID] !== undefined,
|
|
44
|
+
)
|
|
45
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)[0]
|
|
42
46
|
setState(match)
|
|
43
47
|
} catch {
|
|
44
48
|
setState(undefined)
|
|
@@ -63,8 +67,11 @@ export function KeepaliveFooter(props: { theme: Theme; directory: string; sessio
|
|
|
63
67
|
|
|
64
68
|
const interval = session.intervalMs || snapshot.intervalMs || 270_000
|
|
65
69
|
const idleMs = Math.max(0, now() - (session.lastResponseAt || now()))
|
|
70
|
+
// `nextPingAt` is the scheduler's source of truth and includes the
|
|
71
|
+
// per-ping jitter. Counting from it keeps the two counters in step.
|
|
72
|
+
const nextPingAt = Math.max(session.nextPingAt || now(), now())
|
|
66
73
|
const left = session.active
|
|
67
|
-
? Math.max(0, Math.
|
|
74
|
+
? Math.max(0, Math.ceil((session.windowEndsAt - nextPingAt) / interval))
|
|
68
75
|
: 0
|
|
69
76
|
const status: Status =
|
|
70
77
|
!snapshot.enabled || !session.eligible
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@vikrant82/opencode-cache-keepalive",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "OpenCode plugin that keeps provider prompt caches warm by sending invisible keepalive pings, with a live TUI readout",
|
|
7
7
|
"main": "./dist/index.js",
|