@vikrant82/opencode-cache-keepalive 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +255 -56
- package/dist/index.js.map +1 -1
- package/dist/lib/keepalive.d.ts +50 -8
- package/dist/lib/keepalive.d.ts.map +1 -1
- package/dist/lib/state.d.ts +45 -5
- package/dist/lib/state.d.ts.map +1 -1
- package/lib/keepalive.ts +323 -77
- package/lib/state.ts +48 -5
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -147,7 +147,8 @@ var JITTER_MS = 15e3;
|
|
|
147
147
|
var CONTROL_POLL_MS = 1e3;
|
|
148
148
|
var STATUS_POLL_MS = 2e3;
|
|
149
149
|
var PING_STATUS_GRACE_MS = 5e3;
|
|
150
|
-
var
|
|
150
|
+
var COLD_AFTER_INTERVALS = 2;
|
|
151
|
+
var MESSAGE_LOOKUP_LIMIT = 50;
|
|
151
152
|
var KeepaliveEngine = class {
|
|
152
153
|
constructor(client, config, store, logger, directory) {
|
|
153
154
|
this.client = client;
|
|
@@ -190,9 +191,14 @@ var KeepaliveEngine = class {
|
|
|
190
191
|
if (this.statusTimer) clearInterval(this.statusTimer);
|
|
191
192
|
this.statusTimer = void 0;
|
|
192
193
|
}
|
|
193
|
-
/**
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
/**
|
|
195
|
+
* True while a ping turn owns the session, so tool calls must be blocked. Becomes
|
|
196
|
+
* false as soon as a real user message joins the in-flight ping run: the rest of
|
|
197
|
+
* that run is real work and needs its tools.
|
|
198
|
+
*/
|
|
199
|
+
shouldBlockTools(sessionID) {
|
|
200
|
+
const s = this.store.get(sessionID);
|
|
201
|
+
return !!s?.warming && !s.ping?.interrupted;
|
|
196
202
|
}
|
|
197
203
|
async onEvent(event) {
|
|
198
204
|
const type = event?.type;
|
|
@@ -205,19 +211,31 @@ var KeepaliveEngine = class {
|
|
|
205
211
|
}
|
|
206
212
|
return;
|
|
207
213
|
}
|
|
214
|
+
if (type === "message.part.updated") {
|
|
215
|
+
const part = event.properties?.part;
|
|
216
|
+
const sessionID = part?.sessionID ?? event.properties?.sessionID;
|
|
217
|
+
if (!sessionID) return;
|
|
218
|
+
const session = this.store.get(sessionID);
|
|
219
|
+
if (session?.warming) this.notePingRunPart(session, part);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
208
222
|
if (type === "message.updated") {
|
|
209
223
|
const info = event.properties?.info;
|
|
210
224
|
const sessionID = info?.sessionID ?? event.properties?.sessionID;
|
|
211
225
|
if (!sessionID) return;
|
|
212
226
|
const session = this.store.get(sessionID);
|
|
213
|
-
if (session?.warming)
|
|
227
|
+
if (session?.warming) {
|
|
228
|
+
if (info?.role === "user") this.notePingRunUser(session, info);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
214
231
|
const eventAt = info?.time?.completed ?? info?.time?.created;
|
|
215
232
|
if (session?.lastPingAt && typeof eventAt === "number" && eventAt <= session.lastPingAt)
|
|
216
233
|
return;
|
|
217
234
|
if (info?.role === "user") {
|
|
218
235
|
const s = this.store.ensure(sessionID);
|
|
236
|
+
if (!isNewUserMessage(s, info)) return;
|
|
237
|
+
noteUserMessage(s, info);
|
|
219
238
|
s.busy = true;
|
|
220
|
-
s.busySince = Date.now();
|
|
221
239
|
s.active = false;
|
|
222
240
|
this.store.persist();
|
|
223
241
|
return;
|
|
@@ -242,7 +260,6 @@ var KeepaliveEngine = class {
|
|
|
242
260
|
}
|
|
243
261
|
const s = this.store.ensure(sessionID);
|
|
244
262
|
s.busy = true;
|
|
245
|
-
s.busySince = Date.now();
|
|
246
263
|
s.active = false;
|
|
247
264
|
this.store.persist();
|
|
248
265
|
return;
|
|
@@ -254,6 +271,55 @@ var KeepaliveEngine = class {
|
|
|
254
271
|
await this.armWindow(sessionID);
|
|
255
272
|
}
|
|
256
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Track a new user message observed while a ping is in flight. Event order alone
|
|
276
|
+
* cannot tell the ping's own message from a real prompt racing it, so messages are
|
|
277
|
+
* held until their text part arrives (see `notePingRunPart`). Any message seen
|
|
278
|
+
* before the ping is sent, or a second new message, is necessarily a real turn.
|
|
279
|
+
*/
|
|
280
|
+
notePingRunUser(s, info) {
|
|
281
|
+
const ping = s.ping;
|
|
282
|
+
if (!ping || typeof info?.id !== "string") return;
|
|
283
|
+
const created = info?.time?.created;
|
|
284
|
+
if (typeof created === "number" && created < ping.startedAt) return;
|
|
285
|
+
if (info.id === ping.messageID || ping.pending.has(info.id)) return;
|
|
286
|
+
if (!ping.sent || ping.messageID) {
|
|
287
|
+
this.interruptPing(s, info);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
ping.pending.set(info.id, info);
|
|
291
|
+
if (ping.pending.size > 1) this.interruptPing(s);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Classify a pending user message by its first text part: the ping token marks
|
|
295
|
+
* the ping's own message; any other text is a real turn joining the ping run.
|
|
296
|
+
* opencode publishes a user message's parts right after the message itself, before
|
|
297
|
+
* the run reaches any tool call.
|
|
298
|
+
*/
|
|
299
|
+
notePingRunPart(s, part) {
|
|
300
|
+
const ping = s.ping;
|
|
301
|
+
if (!ping || part?.type !== "text" || part?.synthetic) return;
|
|
302
|
+
const id = part?.messageID;
|
|
303
|
+
if (typeof id !== "string" || !ping.pending.has(id)) return;
|
|
304
|
+
const info = ping.pending.get(id);
|
|
305
|
+
ping.pending.delete(id);
|
|
306
|
+
if (!ping.messageID && String(part.text ?? "").trim() === this.config.pingToken) {
|
|
307
|
+
ping.messageID = id;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
this.interruptPing(s, info);
|
|
311
|
+
}
|
|
312
|
+
/** A real turn joined the in-flight ping: unblock tools and treat the session as busy. */
|
|
313
|
+
interruptPing(s, info) {
|
|
314
|
+
const ping = s.ping;
|
|
315
|
+
if (!ping) return;
|
|
316
|
+
if (!ping.interrupted) this.logger.dbg(`real turn joined ping ${short(s.sessionID)}`);
|
|
317
|
+
ping.interrupted = true;
|
|
318
|
+
if (info) noteUserMessage(s, info);
|
|
319
|
+
s.busy = true;
|
|
320
|
+
s.active = false;
|
|
321
|
+
this.store.persist();
|
|
322
|
+
}
|
|
257
323
|
async armWindow(sessionID) {
|
|
258
324
|
const s = this.store.ensure(sessionID);
|
|
259
325
|
if (s.active && !s.busy) return;
|
|
@@ -261,7 +327,6 @@ var KeepaliveEngine = class {
|
|
|
261
327
|
s.arming = true;
|
|
262
328
|
try {
|
|
263
329
|
s.busy = false;
|
|
264
|
-
s.busySince = void 0;
|
|
265
330
|
await this.resolveSession(s);
|
|
266
331
|
if (s.busy || !this.enabled || !s.eligible) {
|
|
267
332
|
s.active = false;
|
|
@@ -269,30 +334,37 @@ var KeepaliveEngine = class {
|
|
|
269
334
|
return;
|
|
270
335
|
}
|
|
271
336
|
const now = Date.now();
|
|
272
|
-
if (!s.lastResponseAt
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
s
|
|
337
|
+
if (!s.lastResponseAt || !this.cacheMayBeWarm(s, now)) {
|
|
338
|
+
s.active = false;
|
|
339
|
+
this.store.persist();
|
|
340
|
+
this.logger.dbg(`not armed ${short(sessionID)} \u2014 no recent real response`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
this.openWindow(s, now);
|
|
279
344
|
this.store.persist();
|
|
280
345
|
this.logger.dbg(`armed ${short(sessionID)} model=${s.modelLabel}`);
|
|
281
346
|
} finally {
|
|
282
347
|
s.arming = false;
|
|
283
348
|
}
|
|
284
349
|
}
|
|
350
|
+
/** Start a warm window anchored on the last real response and the last cache touch. */
|
|
351
|
+
openWindow(s, now) {
|
|
352
|
+
s.idleSince = now;
|
|
353
|
+
s.windowEndsAt = s.lastResponseAt + this.config.windowMs;
|
|
354
|
+
s.nextPingAt = Math.max(now, lastCacheTouch(s) + this.config.intervalMs);
|
|
355
|
+
s.pingsSent = 0;
|
|
356
|
+
s.lastPing = void 0;
|
|
357
|
+
s.active = true;
|
|
358
|
+
}
|
|
359
|
+
/** False once the cache has gone too long without a request to still be trusted warm. */
|
|
360
|
+
cacheMayBeWarm(s, now) {
|
|
361
|
+
const touch = lastCacheTouch(s);
|
|
362
|
+
return touch > 0 && now - touch <= COLD_AFTER_INTERVALS * this.config.intervalMs;
|
|
363
|
+
}
|
|
285
364
|
async tick() {
|
|
286
365
|
if (!this.enabled) return;
|
|
287
366
|
const now = Date.now();
|
|
288
367
|
for (const s of this.store.all()) {
|
|
289
|
-
if (s.busy && s.busySince && now - s.busySince > BUSY_TIMEOUT_MS) {
|
|
290
|
-
this.logger.dbg(`busy timeout ${short(s.sessionID)} \u2014 clearing stuck flag`);
|
|
291
|
-
s.busy = false;
|
|
292
|
-
s.busySince = void 0;
|
|
293
|
-
void this.armWindow(s.sessionID);
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
368
|
if (!s.eligible || !s.active || s.busy || s.warming) continue;
|
|
297
369
|
if (now >= s.windowEndsAt) {
|
|
298
370
|
s.active = false;
|
|
@@ -302,22 +374,65 @@ var KeepaliveEngine = class {
|
|
|
302
374
|
);
|
|
303
375
|
continue;
|
|
304
376
|
}
|
|
305
|
-
if (now
|
|
377
|
+
if (now < s.nextPingAt) continue;
|
|
378
|
+
if (!this.cacheMayBeWarm(s, now)) {
|
|
379
|
+
s.active = false;
|
|
380
|
+
this.store.persist();
|
|
381
|
+
this.logger.dbg(
|
|
382
|
+
`window closed ${short(s.sessionID)} \u2014 ping overdue, cache presumed cold`
|
|
383
|
+
);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
void this.firePing(s.sessionID);
|
|
306
387
|
}
|
|
307
388
|
}
|
|
308
389
|
async firePing(sessionID) {
|
|
309
390
|
const s = this.store.get(sessionID);
|
|
310
|
-
if (!s || s.warming || s.busy) return;
|
|
391
|
+
if (!s || s.warming || s.busy || !s.active) return;
|
|
392
|
+
const ping = {
|
|
393
|
+
startedAt: Date.now(),
|
|
394
|
+
sent: false,
|
|
395
|
+
pending: /* @__PURE__ */ new Map(),
|
|
396
|
+
interrupted: false
|
|
397
|
+
};
|
|
311
398
|
s.warming = true;
|
|
312
|
-
s.
|
|
399
|
+
s.ping = ping;
|
|
313
400
|
this.store.persist();
|
|
314
|
-
const startAt = Date.now();
|
|
315
401
|
try {
|
|
402
|
+
if (!await this.isIdleOnServer(sessionID)) {
|
|
403
|
+
s.busy = true;
|
|
404
|
+
s.active = false;
|
|
405
|
+
this.logger.dbg(`ping ${short(sessionID)} skipped \u2014 session busy`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const request = s.request ?? await this.lookupRequest(s);
|
|
409
|
+
if (!request) {
|
|
410
|
+
s.active = false;
|
|
411
|
+
this.logger.dbg(`ping ${short(sessionID)} skipped \u2014 last real request unknown`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (ping.interrupted || s.busy || !s.active || !this.enabled) return;
|
|
415
|
+
ping.sent = true;
|
|
416
|
+
s.pingsSent += 1;
|
|
417
|
+
this.store.persist();
|
|
316
418
|
const res = await this.client.session.prompt({
|
|
317
419
|
path: { id: sessionID },
|
|
318
|
-
body: {
|
|
420
|
+
body: {
|
|
421
|
+
parts: [{ type: "text", text: this.config.pingToken }],
|
|
422
|
+
...request.agent ? { agent: request.agent } : {},
|
|
423
|
+
model: { providerID: request.providerID, modelID: request.modelID },
|
|
424
|
+
...request.variant ? { variant: request.variant } : {}
|
|
425
|
+
}
|
|
319
426
|
});
|
|
320
427
|
const info = res?.data?.info ?? res?.info;
|
|
428
|
+
if (ping.interrupted) {
|
|
429
|
+
const completed = info?.time?.completed;
|
|
430
|
+
if (typeof completed === "number") s.lastResponseAt = completed;
|
|
431
|
+
this.logger.dbg(
|
|
432
|
+
`ping ${short(sessionID)} absorbed a real turn \u2014 kept, not reverted`
|
|
433
|
+
);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
321
436
|
const cache = info?.tokens?.cache ?? {};
|
|
322
437
|
const record = {
|
|
323
438
|
at: Date.now(),
|
|
@@ -331,40 +446,94 @@ var KeepaliveEngine = class {
|
|
|
331
446
|
this.logger.dbg(
|
|
332
447
|
`ping ${short(sessionID)} ${record.hit ? "HIT" : "MISS"} in=${record.input} read=${record.cacheRead} write=${record.cacheWrite} out=${record.output}`
|
|
333
448
|
);
|
|
334
|
-
if (this.config.revertPing) await this.revertPing(sessionID,
|
|
449
|
+
if (this.config.revertPing) await this.revertPing(sessionID, ping);
|
|
335
450
|
} catch (err) {
|
|
336
451
|
this.logger.warn(`ping ${short(sessionID)} failed`, errText(err));
|
|
337
452
|
} finally {
|
|
338
453
|
s.warming = false;
|
|
339
|
-
s.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
s.
|
|
343
|
-
|
|
454
|
+
s.ping = void 0;
|
|
455
|
+
const now = Date.now();
|
|
456
|
+
if (ping.sent) {
|
|
457
|
+
s.lastPingAt = now;
|
|
458
|
+
const next = s.nextPingAt + jitter(this.config.intervalMs);
|
|
459
|
+
s.nextPingAt = next > now ? next : now + jitter(this.config.intervalMs);
|
|
460
|
+
}
|
|
344
461
|
this.store.persist();
|
|
462
|
+
if (ping.sent && ping.interrupted) {
|
|
463
|
+
s.busy = false;
|
|
464
|
+
s.active = false;
|
|
465
|
+
void this.armWindow(sessionID);
|
|
466
|
+
}
|
|
345
467
|
}
|
|
346
468
|
}
|
|
347
|
-
/**
|
|
348
|
-
async
|
|
349
|
-
let messages;
|
|
469
|
+
/** True only when the server positively reports the session idle. */
|
|
470
|
+
async isIdleOnServer(sessionID) {
|
|
350
471
|
try {
|
|
351
|
-
const res = await this.client.session.
|
|
352
|
-
|
|
472
|
+
const res = await this.client.session.status();
|
|
473
|
+
const statuses = res?.data ?? res ?? {};
|
|
474
|
+
return (statuses?.[sessionID]?.type ?? "idle") === "idle";
|
|
353
475
|
} catch (err) {
|
|
354
|
-
this.logger.
|
|
355
|
-
return;
|
|
476
|
+
this.logger.dbg(`status check ${short(sessionID)} failed`, errText(err));
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/** Recover the last real turn's request settings when no user event was observed. */
|
|
481
|
+
async lookupRequest(s) {
|
|
482
|
+
const messages = await this.recentMessages(s.sessionID);
|
|
483
|
+
if (!messages) return void 0;
|
|
484
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
485
|
+
const m = messages[i];
|
|
486
|
+
if (m?.info?.role !== "user" || this.isPingMessage(m)) continue;
|
|
487
|
+
const request = requestFrom(m.info);
|
|
488
|
+
if (request) {
|
|
489
|
+
s.request = request;
|
|
490
|
+
return request;
|
|
491
|
+
}
|
|
356
492
|
}
|
|
357
|
-
|
|
358
|
-
|
|
493
|
+
return void 0;
|
|
494
|
+
}
|
|
495
|
+
async recentMessages(sessionID) {
|
|
496
|
+
try {
|
|
497
|
+
const res = await this.client.session.messages({
|
|
498
|
+
path: { id: sessionID },
|
|
499
|
+
query: { limit: MESSAGE_LOOKUP_LIMIT }
|
|
500
|
+
});
|
|
501
|
+
const messages = res?.data ?? res;
|
|
502
|
+
return Array.isArray(messages) ? messages : void 0;
|
|
503
|
+
} catch (err) {
|
|
504
|
+
this.logger.warn(`messages ${short(sessionID)} list failed`, errText(err));
|
|
505
|
+
return void 0;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
isPingMessage(m) {
|
|
509
|
+
const text = (m?.parts ?? []).filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("").trim();
|
|
510
|
+
return text === this.config.pingToken;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Remove the `~` user + `~` assistant turn so it never enters real context.
|
|
514
|
+
* Revert drops the target and everything after it (and rolls back file
|
|
515
|
+
* snapshots), so it is skipped unless only the ping's own replies follow.
|
|
516
|
+
*/
|
|
517
|
+
async revertPing(sessionID, ping) {
|
|
518
|
+
const messages = await this.recentMessages(sessionID);
|
|
519
|
+
if (!messages) return;
|
|
520
|
+
let index = -1;
|
|
521
|
+
for (let i = 0; i < messages.length; i++) {
|
|
522
|
+
const m = messages[i];
|
|
359
523
|
if (m?.info?.role !== "user") continue;
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
if (text === this.config.pingToken) target = m.info.id;
|
|
524
|
+
if (ping.messageID ? m.info.id === ping.messageID : this.isRecentPing(m, ping))
|
|
525
|
+
index = i;
|
|
363
526
|
}
|
|
364
|
-
if (
|
|
527
|
+
if (index < 0) {
|
|
365
528
|
this.logger.dbg(`ping ${short(sessionID)} revert: ping message not found`);
|
|
366
529
|
return;
|
|
367
530
|
}
|
|
531
|
+
const target = messages[index].info.id;
|
|
532
|
+
const foreign = messages.slice(index + 1).some((m) => m?.info?.role !== "assistant" || m?.info?.parentID !== target);
|
|
533
|
+
if (foreign) {
|
|
534
|
+
this.logger.warn(`ping ${short(sessionID)} not reverted \u2014 other messages followed it`);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
368
537
|
try {
|
|
369
538
|
await this.client.session.revert({
|
|
370
539
|
path: { id: sessionID },
|
|
@@ -374,6 +543,9 @@ var KeepaliveEngine = class {
|
|
|
374
543
|
this.logger.warn(`ping ${short(sessionID)} revert failed`, errText(err));
|
|
375
544
|
}
|
|
376
545
|
}
|
|
546
|
+
isRecentPing(m, ping) {
|
|
547
|
+
return Number(m?.info?.time?.created ?? 0) >= ping.startedAt - 2e3 && this.isPingMessage(m);
|
|
548
|
+
}
|
|
377
549
|
async resolveSession(s) {
|
|
378
550
|
try {
|
|
379
551
|
const res = await this.client.session.get({ path: { id: s.sessionID } });
|
|
@@ -404,13 +576,8 @@ var KeepaliveEngine = class {
|
|
|
404
576
|
continue;
|
|
405
577
|
}
|
|
406
578
|
if (!session.eligible || session.busy || session.warming) continue;
|
|
407
|
-
if (!session.lastResponseAt) continue;
|
|
408
|
-
session
|
|
409
|
-
session.windowEndsAt = now + this.config.windowMs;
|
|
410
|
-
session.nextPingAt = now + this.config.intervalMs;
|
|
411
|
-
session.pingsSent = 0;
|
|
412
|
-
session.lastPing = void 0;
|
|
413
|
-
session.active = true;
|
|
579
|
+
if (!session.lastResponseAt || !this.cacheMayBeWarm(session, now)) continue;
|
|
580
|
+
this.openWindow(session, now);
|
|
414
581
|
}
|
|
415
582
|
this.store.persist();
|
|
416
583
|
this.logger.info(`runtime ${control.enabled ? "enabled" : "disabled"}`);
|
|
@@ -435,6 +602,34 @@ var KeepaliveEngine = class {
|
|
|
435
602
|
}
|
|
436
603
|
}
|
|
437
604
|
};
|
|
605
|
+
function lastCacheTouch(s) {
|
|
606
|
+
return Math.max(s.lastResponseAt, s.lastPingAt ?? 0);
|
|
607
|
+
}
|
|
608
|
+
function isNewUserMessage(s, info) {
|
|
609
|
+
if (typeof info?.id === "string" && info.id === s.lastUserID) return false;
|
|
610
|
+
const created = info?.time?.created;
|
|
611
|
+
return typeof created !== "number" || created >= (s.lastUserAt ?? 0);
|
|
612
|
+
}
|
|
613
|
+
function noteUserMessage(s, info) {
|
|
614
|
+
if (typeof info?.id === "string") s.lastUserID = info.id;
|
|
615
|
+
const created = info?.time?.created;
|
|
616
|
+
if (typeof created === "number") s.lastUserAt = created;
|
|
617
|
+
const request = requestFrom(info);
|
|
618
|
+
if (request) s.request = request;
|
|
619
|
+
}
|
|
620
|
+
function requestFrom(info) {
|
|
621
|
+
const model = info?.model;
|
|
622
|
+
const providerID = model?.providerID;
|
|
623
|
+
const modelID = model?.modelID ?? model?.id;
|
|
624
|
+
if (typeof providerID !== "string" || typeof modelID !== "string") return void 0;
|
|
625
|
+
const variant = model?.variant ?? info?.variant;
|
|
626
|
+
return {
|
|
627
|
+
agent: typeof info?.agent === "string" ? info.agent : void 0,
|
|
628
|
+
providerID,
|
|
629
|
+
modelID,
|
|
630
|
+
variant: typeof variant === "string" && variant !== "default" ? variant : void 0
|
|
631
|
+
};
|
|
632
|
+
}
|
|
438
633
|
function short(sessionID) {
|
|
439
634
|
return sessionID.slice(-6);
|
|
440
635
|
}
|
|
@@ -529,7 +724,10 @@ var KeepaliveStore = class {
|
|
|
529
724
|
all() {
|
|
530
725
|
return [...this.sessions.values()];
|
|
531
726
|
}
|
|
532
|
-
/**
|
|
727
|
+
/**
|
|
728
|
+
* Atomically persist a lean snapshot for the TUI. Never throws or rejects; the
|
|
729
|
+
* returned promise resolves once this snapshot and every earlier one are written.
|
|
730
|
+
*/
|
|
533
731
|
persist() {
|
|
534
732
|
const snapshot = {
|
|
535
733
|
version: 1,
|
|
@@ -559,6 +757,7 @@ var KeepaliveStore = class {
|
|
|
559
757
|
};
|
|
560
758
|
const write = () => atomicWrite(stateFilePath(this.directory), JSON.stringify(snapshot));
|
|
561
759
|
this.writeQueue = this.writeQueue.then(write, write);
|
|
760
|
+
return this.writeQueue;
|
|
562
761
|
}
|
|
563
762
|
};
|
|
564
763
|
async function atomicWrite(path, contents) {
|
|
@@ -596,7 +795,7 @@ var server = async (ctx, options) => {
|
|
|
596
795
|
const store = new KeepaliveStore(config, ctx.directory);
|
|
597
796
|
const engine = new KeepaliveEngine(ctx.client, config, store, logger, ctx.directory);
|
|
598
797
|
engine.start();
|
|
599
|
-
const version = true ? "0.1.
|
|
798
|
+
const version = true ? "0.1.5" : "dev";
|
|
600
799
|
logger.info(`v${version} ready`);
|
|
601
800
|
const instruction = keepaliveInstruction(config.pingToken);
|
|
602
801
|
return {
|
|
@@ -614,7 +813,7 @@ var server = async (ctx, options) => {
|
|
|
614
813
|
// Backstop: if the model ignores the instruction and tries a tool during a
|
|
615
814
|
// ping, block it — the ping turn is going to be reverted anyway.
|
|
616
815
|
"tool.execute.before": async (input) => {
|
|
617
|
-
if (engine.
|
|
816
|
+
if (engine.shouldBlockTools(input.sessionID)) {
|
|
618
817
|
throw new Error("cache-keepalive: tools are disabled during a keepalive ping");
|
|
619
818
|
}
|
|
620
819
|
},
|