@vikrant82/opencode-cache-keepalive 0.1.5 → 0.2.0
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/MIGRATION.md +57 -0
- package/README.md +75 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1097 -614
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +8 -25
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/control.d.ts +10 -4
- package/dist/lib/control.d.ts.map +1 -1
- package/dist/lib/paths.d.ts +5 -12
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/replay-warmer.d.ts +95 -0
- package/dist/lib/replay-warmer.d.ts.map +1 -0
- package/dist/lib/state.d.ts +15 -114
- package/dist/lib/state.d.ts.map +1 -1
- package/dist/lib/tui/commands.d.ts +2 -0
- package/dist/lib/tui/commands.d.ts.map +1 -1
- package/dist/lib/tui/footer.d.ts +5 -5
- 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/dist/lib/types.d.ts +123 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/tui.d.ts +3 -0
- package/dist/tui.d.ts.map +1 -1
- package/lib/config.ts +106 -74
- package/lib/control.ts +69 -20
- package/lib/paths.ts +12 -14
- package/lib/replay-warmer.ts +1200 -0
- package/lib/state.ts +172 -175
- package/lib/tui/commands.tsx +76 -0
- package/lib/tui/footer.tsx +55 -130
- package/lib/tui/format.ts +40 -0
- package/lib/types.ts +130 -0
- package/package.json +3 -2
- package/tui.tsx +40 -0
- package/dist/lib/keepalive.d.ts +0 -85
- package/dist/lib/keepalive.d.ts.map +0 -1
- package/dist/lib/model.d.ts +0 -8
- package/dist/lib/model.d.ts.map +0 -1
- package/dist/lib/system.d.ts +0 -11
- package/dist/lib/system.d.ts.map +0 -1
- package/lib/keepalive.ts +0 -636
- package/lib/model.ts +0 -19
- package/lib/system.ts +0 -19
- package/lib/tui/commands.ts +0 -59
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-warmer.d.ts","sourceRoot":"","sources":["../../lib/replay-warmer.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -1,118 +1,19 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
cacheRead: number;
|
|
7
|
-
cacheWrite: number;
|
|
8
|
-
output: number;
|
|
1
|
+
import type { Logger } from "./logger";
|
|
2
|
+
import type { ProcessTotals, SessionWarmEntry, WarmStateSink } from "./types";
|
|
3
|
+
export type StateStoreOptions = {
|
|
4
|
+
logger?: Logger;
|
|
5
|
+
now?: () => number;
|
|
9
6
|
};
|
|
10
7
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* the session's persisted agent/model/variant.
|
|
8
|
+
* Create an isolated v2 snapshot writer. Mutations are debounced, dispose flushes
|
|
9
|
+
* the latest snapshot before removing it, and all filesystem failures stay
|
|
10
|
+
* best-effort so state reporting cannot disrupt replay execution.
|
|
15
11
|
*/
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export type InflightPing = {
|
|
24
|
-
/** Epoch ms the ping attempt began (before the pre-flight status check). */
|
|
25
|
-
startedAt: number;
|
|
26
|
-
/** The prompt request has been issued. */
|
|
27
|
-
sent: boolean;
|
|
28
|
-
/** The ping's own user message, identified by its ping-token text part. */
|
|
29
|
-
messageID?: string;
|
|
30
|
-
/**
|
|
31
|
-
* New user messages seen during the ping whose text has not yet been observed.
|
|
32
|
-
* Each is classified by its first text part: the ping token marks our own
|
|
33
|
-
* message, anything else a real turn.
|
|
34
|
-
*/
|
|
35
|
-
pending: Map<string, unknown>;
|
|
36
|
-
/**
|
|
37
|
-
* A real user message appeared while the ping was in flight. opencode joins such
|
|
38
|
-
* a prompt to the running ping loop, so the rest of the run is real work.
|
|
39
|
-
*/
|
|
40
|
-
interrupted: boolean;
|
|
41
|
-
};
|
|
42
|
-
/** Live per-session bookkeeping held by the server plugin. */
|
|
43
|
-
export type SessionKeepalive = {
|
|
44
|
-
sessionID: string;
|
|
45
|
-
/** Whether the session's provider/model pair supports cache warming. */
|
|
46
|
-
eligible: boolean;
|
|
47
|
-
modelLabel?: string;
|
|
48
|
-
/** Epoch ms of the last real assistant response (excludes ping replies). */
|
|
49
|
-
lastResponseAt: number;
|
|
50
|
-
/** Epoch ms the current idle stretch began. */
|
|
51
|
-
idleSince: number;
|
|
52
|
-
/** Epoch ms after which warming stops and the cache is allowed to go cold. */
|
|
53
|
-
windowEndsAt: number;
|
|
54
|
-
/** Epoch ms the next ping is due. */
|
|
55
|
-
nextPingAt: number;
|
|
56
|
-
/** Number of scheduled ping requests attempted during this warm window. */
|
|
57
|
-
pingsSent: number;
|
|
58
|
-
/** A real turn is currently running. */
|
|
59
|
-
busy: boolean;
|
|
60
|
-
/** A keepalive ping is currently in flight. */
|
|
61
|
-
warming: boolean;
|
|
62
|
-
/** The in-flight ping; set exactly while `warming` is true. */
|
|
63
|
-
ping?: InflightPing;
|
|
64
|
-
/** ID and creation time of the newest user message seen, to tell new turns from re-emitted old messages. */
|
|
65
|
-
lastUserID?: string;
|
|
66
|
-
lastUserAt?: number;
|
|
67
|
-
/** Settings of the last real user turn, replayed by pings. */
|
|
68
|
-
request?: PingRequest;
|
|
69
|
-
/** Within the warm window and actively scheduling pings. */
|
|
70
|
-
active: boolean;
|
|
71
|
-
/** True while `armWindow` is resolving session metadata (async guard). */
|
|
72
|
-
arming?: boolean;
|
|
73
|
-
/** Epoch ms the last sent ping finished; ignores its late events and marks the last cache touch. */
|
|
74
|
-
lastPingAt?: number;
|
|
75
|
-
lastPing?: PingRecord;
|
|
76
|
-
};
|
|
77
|
-
/** Lean projection persisted to disk for the TUI reader. */
|
|
78
|
-
export type PersistedSession = {
|
|
79
|
-
eligible: boolean;
|
|
80
|
-
modelLabel?: string;
|
|
81
|
-
lastResponseAt: number;
|
|
82
|
-
idleSince: number;
|
|
83
|
-
windowEndsAt: number;
|
|
84
|
-
nextPingAt: number;
|
|
85
|
-
intervalMs: number;
|
|
86
|
-
pingsSent: number;
|
|
87
|
-
busy: boolean;
|
|
88
|
-
warming: boolean;
|
|
89
|
-
active: boolean;
|
|
90
|
-
lastPing?: PingRecord;
|
|
91
|
-
};
|
|
92
|
-
export type PersistedState = {
|
|
93
|
-
version: 1;
|
|
94
|
-
updatedAt: number;
|
|
95
|
-
enabled: boolean;
|
|
96
|
-
intervalMs: number;
|
|
97
|
-
windowMs: number;
|
|
98
|
-
sessions: Record<string, PersistedSession>;
|
|
99
|
-
};
|
|
100
|
-
export declare class KeepaliveStore {
|
|
101
|
-
private readonly config;
|
|
102
|
-
private readonly directory;
|
|
103
|
-
private readonly sessions;
|
|
104
|
-
private writeQueue;
|
|
105
|
-
private enabled;
|
|
106
|
-
constructor(config: KeepaliveConfig, directory: string);
|
|
107
|
-
setEnabled(enabled: boolean): void;
|
|
108
|
-
get(sessionID: string): SessionKeepalive | undefined;
|
|
109
|
-
ensure(sessionID: string): SessionKeepalive;
|
|
110
|
-
remove(sessionID: string): void;
|
|
111
|
-
all(): SessionKeepalive[];
|
|
112
|
-
/**
|
|
113
|
-
* Atomically persist a lean snapshot for the TUI. Never throws or rejects; the
|
|
114
|
-
* returned promise resolves once this snapshot and every earlier one are written.
|
|
115
|
-
*/
|
|
116
|
-
persist(): Promise<void>;
|
|
117
|
-
}
|
|
12
|
+
export declare function createStateStore(directory: string, options?: StateStoreOptions): WarmStateSink;
|
|
13
|
+
/** Return the newest self-contained entry for a session across matching v2 files. */
|
|
14
|
+
export declare function readSessionEntry(directory: string, sessionID: string): SessionWarmEntry | undefined;
|
|
15
|
+
/** Sum process-level totals from live snapshots for this directory. */
|
|
16
|
+
export declare function readProcessTotals(directory: string): ProcessTotals;
|
|
17
|
+
/** Remove only v2 snapshots whose owner PID is confirmed dead. */
|
|
18
|
+
export declare function cleanupStateFiles(directory: string, logger?: Logger): void;
|
|
118
19
|
//# sourceMappingURL=state.d.ts.map
|
package/dist/lib/state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../lib/state.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../lib/state.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAEtC,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAc7E,MAAM,MAAM,iBAAiB,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAAE,CAAA;AAEvE;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,iBAAsB,GAChC,aAAa,CAoFf;AAED,qFAAqF;AACrF,wBAAgB,gBAAgB,CAC5B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAClB,gBAAgB,GAAG,SAAS,CAO9B;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CAgBlE;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAmB1E"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
1
2
|
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
3
|
+
/** Register runtime enable/disable commands for the current session only. */
|
|
2
4
|
export declare function registerKeepaliveCommands(api: TuiPluginApi): void;
|
|
3
5
|
//# 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,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAG3D,6EAA6E;AAC7E,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAqEjE"}
|
package/dist/lib/tui/footer.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { SessionWarmEntry } from "../types";
|
|
2
3
|
type Theme = Record<string, any>;
|
|
3
|
-
/**
|
|
4
|
-
* Persistent readout mounted in the `sidebar_footer` slot. Shows time since the
|
|
5
|
-
* last real agent response plus keepalive pings sent / left. Data comes from the
|
|
6
|
-
* server plugin's state file; the idle timer ticks locally every second.
|
|
7
|
-
*/
|
|
4
|
+
/** Display the persisted replay-warming state for the current session only. */
|
|
8
5
|
export declare function KeepaliveFooter(props: {
|
|
9
6
|
theme: Theme;
|
|
10
7
|
directory: string;
|
|
11
8
|
sessionID?: string;
|
|
12
9
|
}): any;
|
|
10
|
+
/** Pure formatter for the two-line footer's optional telemetry. */
|
|
11
|
+
export declare function formatDetails(entry: SessionWarmEntry, now?: number): string | undefined;
|
|
12
|
+
export declare function formatStatusLine(entry: SessionWarmEntry, remainingMs: number): string;
|
|
13
13
|
export {};
|
|
14
14
|
//# sourceMappingURL=footer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../lib/tui/footer.tsx"],"names":[],"mappings":"AAAA,sCAAsC;
|
|
1
|
+
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../lib/tui/footer.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AAItC,OAAO,KAAK,EAAE,gBAAgB,EAAa,MAAM,UAAU,CAAA;AAI3D,KAAK,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAEhC,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,KAAK,EAAE;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,OAkD7F;AAED,mEAAmE;AACnE,wBAAgB,aAAa,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,SAAa,GAAG,MAAM,GAAG,SAAS,CAE3F;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAYrF"}
|
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,
|
|
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,CAStC;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"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared contracts between the server-side replay engine and the state/TUI layer.
|
|
3
|
+
*
|
|
4
|
+
* The engine produces one self-contained {@link SessionWarmEntry} per tracked root
|
|
5
|
+
* session and hands it to a {@link WarmStateSink}; the TUI reads persisted entries
|
|
6
|
+
* back per session. Entries never contain request URLs with queries, headers,
|
|
7
|
+
* bodies, credentials, or conversation text.
|
|
8
|
+
*/
|
|
9
|
+
/** Wire API family of a recorded request, derived from the request pathname. */
|
|
10
|
+
export type WarmApi = "messages" | "responses" | "chat";
|
|
11
|
+
/**
|
|
12
|
+
* Display/lifecycle state of a session's cache warming.
|
|
13
|
+
* - `active`: a real model request/step is in flight; no replay is sent.
|
|
14
|
+
* - `busy`: the session is blocked on a running tool; replays are scheduled.
|
|
15
|
+
* - `idle`: the session is idle between turns; replays are scheduled.
|
|
16
|
+
* - `stopped`: warming ended for the current gap; see {@link StopReason}.
|
|
17
|
+
* - `off`: warming is disabled by config or runtime control.
|
|
18
|
+
*/
|
|
19
|
+
export type WarmState = "active" | "busy" | "idle" | "stopped" | "off";
|
|
20
|
+
/**
|
|
21
|
+
* Why warming stopped for the current gap.
|
|
22
|
+
* - `cap`: the per-gap replay cap was reached.
|
|
23
|
+
* - `lapsed`: the cache is believed expired (failed retry, or a replay reported a cache write instead of a read).
|
|
24
|
+
* - `rejected-4xx`: the provider rejected the replay with 400/401/403.
|
|
25
|
+
* - `disabled`: runtime control or config turned warming off.
|
|
26
|
+
*/
|
|
27
|
+
export type StopReason = "cap" | "lapsed" | "rejected-4xx" | "disabled";
|
|
28
|
+
/** Outcome of the most recent replay attempt. */
|
|
29
|
+
export interface ReplayResult {
|
|
30
|
+
/** Replay send time, epoch ms. */
|
|
31
|
+
at: number;
|
|
32
|
+
/** HTTP status; 0 for network error or timeout. */
|
|
33
|
+
status: number;
|
|
34
|
+
/** Milliseconds from issuing the replay fetch to the API-specific abort point. */
|
|
35
|
+
latencyMs: number;
|
|
36
|
+
/** Cached prompt tokens reported by the provider, when parseable before abort (messages API). */
|
|
37
|
+
cacheRead?: number;
|
|
38
|
+
/** Cache-write prompt tokens reported by the provider, when parseable before abort (messages API). */
|
|
39
|
+
cacheWrite?: number;
|
|
40
|
+
}
|
|
41
|
+
/** Result of the first real model call after a gap that had at least one successful replay. */
|
|
42
|
+
export interface ResumeResult {
|
|
43
|
+
/** step-finish time, epoch ms. */
|
|
44
|
+
at: number;
|
|
45
|
+
/** cache.read tokens of that call. */
|
|
46
|
+
read: number;
|
|
47
|
+
/** Prompt tokens (input + cache.read + cache.write) of the last call before the gap. */
|
|
48
|
+
promptPrev: number;
|
|
49
|
+
/** True when read >= 0.9 * promptPrev. */
|
|
50
|
+
hit: boolean;
|
|
51
|
+
/** Successful replays spent in the gap. */
|
|
52
|
+
replays: number;
|
|
53
|
+
/**
|
|
54
|
+
* Cache-write tokens avoided for the gap: `promptPrev` on a hit, 0 on a miss.
|
|
55
|
+
* Gross figure shown to users as "saved"; replay cost is reported separately.
|
|
56
|
+
*/
|
|
57
|
+
avoidedTokens: number;
|
|
58
|
+
/**
|
|
59
|
+
* Cached-read tokens spent by the gap's successful replays (sum of each replay's
|
|
60
|
+
* reported cacheRead, falling back to promptPrev when not parsed). Logged only.
|
|
61
|
+
*/
|
|
62
|
+
replayReadTokens: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Complete, self-contained display state for one root session. Every field the TUI
|
|
66
|
+
* needs is here so readers never depend on file-level or process-level fields.
|
|
67
|
+
*/
|
|
68
|
+
export interface SessionWarmEntry {
|
|
69
|
+
sessionID: string;
|
|
70
|
+
/** Last time this entry changed, epoch ms; readers pick the freshest entry per session. */
|
|
71
|
+
updatedAt: number;
|
|
72
|
+
/** Effective enablement (config AND runtime control). */
|
|
73
|
+
enabled: boolean;
|
|
74
|
+
/** Model id from the recorded request body. */
|
|
75
|
+
model: string;
|
|
76
|
+
api: WarmApi;
|
|
77
|
+
state: WarmState;
|
|
78
|
+
stopReason?: StopReason;
|
|
79
|
+
/** HTTP status that caused `rejected-4xx`. */
|
|
80
|
+
stopStatus?: number;
|
|
81
|
+
/** Monotonic per-session gap counter; a gap starts at each real request send. */
|
|
82
|
+
gapId: number;
|
|
83
|
+
/** Successful replays in the current gap. */
|
|
84
|
+
gapReplays: number;
|
|
85
|
+
/** Per-gap replay cap in effect. */
|
|
86
|
+
cap: number;
|
|
87
|
+
/** Next scheduled replay time, epoch ms; null when none is scheduled. */
|
|
88
|
+
nextReplayAt: number | null;
|
|
89
|
+
/** Successful replays across the session's lifetime in this process. */
|
|
90
|
+
sessionReplays: number;
|
|
91
|
+
/** Sum of ResumeResult.avoidedTokens across the session's gaps in this process. */
|
|
92
|
+
avoidedTokens: number;
|
|
93
|
+
/** Resumes (first call after a gap with ≥1 successful replay) that hit, in this process. */
|
|
94
|
+
resumeHits: number;
|
|
95
|
+
/** All resumes (hits + misses) in this process. */
|
|
96
|
+
resumeCount: number;
|
|
97
|
+
lastReplay?: ReplayResult;
|
|
98
|
+
resume?: ResumeResult;
|
|
99
|
+
}
|
|
100
|
+
/** Process-wide aggregates, informational only (never used to gate display). */
|
|
101
|
+
export interface ProcessTotals {
|
|
102
|
+
replays: number;
|
|
103
|
+
avoidedTokens: number;
|
|
104
|
+
replayReadTokens: number;
|
|
105
|
+
resumeHits: number;
|
|
106
|
+
resumeMisses: number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Destination for engine state. Implementations persist sanitized entries for the
|
|
110
|
+
* TUI. Calls are synchronous and must never throw; persistence may be debounced.
|
|
111
|
+
* Single-threaded use from the plugin's event loop is assumed.
|
|
112
|
+
*/
|
|
113
|
+
export interface WarmStateSink {
|
|
114
|
+
/** Replace the entry for `entry.sessionID`. */
|
|
115
|
+
upsert(entry: SessionWarmEntry): void;
|
|
116
|
+
/** Forget a session (e.g. deleted). */
|
|
117
|
+
remove(sessionID: string): void;
|
|
118
|
+
/** Replace process totals. */
|
|
119
|
+
setTotals(totals: ProcessTotals): void;
|
|
120
|
+
/** Flush pending writes and remove this instance's persisted file. Never rejects. */
|
|
121
|
+
dispose(): Promise<void>;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../lib/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,gFAAgF;AAChF,MAAM,MAAM,OAAO,GAAG,UAAU,GAAG,WAAW,GAAG,MAAM,CAAA;AAEvD;;;;;;;GAOG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,KAAK,CAAA;AAEtE;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,cAAc,GAAG,UAAU,CAAA;AAEvE,iDAAiD;AACjD,MAAM,WAAW,YAAY;IACzB,kCAAkC;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAA;IACd,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAA;IACjB,iGAAiG;IACjG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sGAAsG;IACtG,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,+FAA+F;AAC/F,MAAM,WAAW,YAAY;IACzB,kCAAkC;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,wFAAwF;IACxF,UAAU,EAAE,MAAM,CAAA;IAClB,0CAA0C;IAC1C,GAAG,EAAE,OAAO,CAAA;IACZ,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAA;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,2FAA2F;IAC3F,SAAS,EAAE,MAAM,CAAA;IACjB,yDAAyD;IACzD,OAAO,EAAE,OAAO,CAAA;IAChB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,OAAO,CAAA;IACZ,KAAK,EAAE,SAAS,CAAA;IAChB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAA;IAClB,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAA;IACX,yEAAyE;IACzE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,wEAAwE;IACxE,cAAc,EAAE,MAAM,CAAA;IACtB,mFAAmF;IACnF,aAAa,EAAE,MAAM,CAAA;IACrB,4FAA4F;IAC5F,UAAU,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,YAAY,CAAA;IACzB,MAAM,CAAC,EAAE,YAAY,CAAA;CACxB;AAED,gFAAgF;AAChF,MAAM,WAAW,aAAa;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC1B,+CAA+C;IAC/C,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACrC,uCAAuC;IACvC,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,8BAA8B;IAC9B,SAAS,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IACtC,qFAAqF;IACrF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B"}
|
package/dist/tui.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { SessionWarmEntry } from "./lib/types";
|
|
3
|
+
/** Route-level owner for deduplicating rejected replay notifications. */
|
|
4
|
+
export declare function notifyRejectedReplayOnce(notified: Set<string>, sessionID: string, entry: SessionWarmEntry, toast: (message: string) => void): void;
|
|
2
5
|
declare const _default: {
|
|
3
6
|
id: string;
|
|
4
7
|
tui: import("@opencode-ai/plugin/tui").TuiPlugin;
|
package/dist/tui.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../tui.tsx"],"names":[],"mappings":"AAAA,sCAAsC;;;;;
|
|
1
|
+
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../tui.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AAMtC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD,yEAAyE;AACzE,wBAAgB,wBAAwB,CACpC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,EACrB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GACjC,IAAI,CAWN;;;;;AAuCD,wBAAgF"}
|
package/lib/config.ts
CHANGED
|
@@ -1,101 +1,133 @@
|
|
|
1
1
|
export type KeepaliveConfig = {
|
|
2
|
-
/** Master switch. When false the plugin registers no timers or hooks. */
|
|
3
2
|
enabled: boolean
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* write/read break-even; providers with automatic caching may be cheaper.
|
|
10
|
-
*/
|
|
11
|
-
windowMs: number
|
|
12
|
-
/**
|
|
13
|
-
* Append a stable keepalive instruction to the system prompt so the model
|
|
14
|
-
* answers a `~` ping with a single `~` token and never calls tools. The
|
|
15
|
-
* instruction is added to real turns too, so the cached prefix stays identical.
|
|
16
|
-
*/
|
|
17
|
-
injectSystemInstruction: boolean
|
|
18
|
-
/** The single character/token used as the keepalive suffix. */
|
|
19
|
-
pingToken: string
|
|
20
|
-
/** Remove synthetic ping turns after measuring their cache usage. */
|
|
21
|
-
revertPing: boolean
|
|
22
|
-
/** Warm subagent/child sessions too. Off by default to avoid wasted pings. */
|
|
3
|
+
intervals: Record<string, number>
|
|
4
|
+
hosts: string[]
|
|
5
|
+
cacheReadFactor: number
|
|
6
|
+
missFactor: number
|
|
7
|
+
maxReplaysPerGap: "auto" | number
|
|
23
8
|
includeChildSessions: boolean
|
|
9
|
+
replayTimeoutMs: number
|
|
10
|
+
maxStoredBytes: number
|
|
24
11
|
debug: boolean
|
|
25
|
-
/** providerID substrings eligible for warming (cache-controlled providers). */
|
|
26
|
-
providerAllowlist: string[]
|
|
27
|
-
/** modelID substrings eligible for warming. */
|
|
28
|
-
modelAllowlist: string[]
|
|
29
12
|
}
|
|
30
13
|
|
|
31
14
|
const DEFAULTS: KeepaliveConfig = {
|
|
32
15
|
enabled: true,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
16
|
+
intervals: { claude: 285_000, gpt: 1_680_000 },
|
|
17
|
+
hosts: ["githubcopilot.com"],
|
|
18
|
+
cacheReadFactor: 0.1,
|
|
19
|
+
missFactor: 1,
|
|
20
|
+
maxReplaysPerGap: "auto",
|
|
38
21
|
includeChildSessions: false,
|
|
22
|
+
replayTimeoutMs: 60_000,
|
|
23
|
+
maxStoredBytes: 67_108_864,
|
|
39
24
|
debug: false,
|
|
40
|
-
providerAllowlist: ["copilot"],
|
|
41
|
-
modelAllowlist: ["claude", "anthropic", "sonnet", "opus", "haiku", "gpt"],
|
|
42
25
|
}
|
|
43
26
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
27
|
+
const DEPRECATED = [
|
|
28
|
+
"intervalMs",
|
|
29
|
+
"intervalSeconds",
|
|
30
|
+
"windowMs",
|
|
31
|
+
"windowMinutes",
|
|
32
|
+
"revertPing",
|
|
33
|
+
"pingToken",
|
|
34
|
+
"injectSystemInstruction",
|
|
35
|
+
"providerAllowlist",
|
|
36
|
+
"modelAllowlist",
|
|
37
|
+
"claudeBusyWarm",
|
|
38
|
+
"claudeBusyWarmIntervalMs",
|
|
39
|
+
"claudeBusyWarmWindowMs",
|
|
40
|
+
]
|
|
41
|
+
const warnedDeprecated = new Set<string>()
|
|
47
42
|
|
|
43
|
+
export function getConfig(
|
|
44
|
+
options: Record<string, unknown> | undefined,
|
|
45
|
+
warn: (message: string) => void = () => {},
|
|
46
|
+
): KeepaliveConfig {
|
|
47
|
+
const option = options ?? {}
|
|
48
|
+
const env = process.env
|
|
49
|
+
for (const key of DEPRECATED) {
|
|
50
|
+
if (key in option && !warnedDeprecated.has(key)) {
|
|
51
|
+
warnedDeprecated.add(key)
|
|
52
|
+
warn(`deprecated option ${key} is ignored`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const parseJson = (raw: string | undefined): unknown => {
|
|
56
|
+
if (!raw) return undefined
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(raw)
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const intervals = objectOption(
|
|
64
|
+
option.intervals ?? parseJson(env.OPENCODE_KEEPALIVE_INTERVALS),
|
|
65
|
+
DEFAULTS.intervals,
|
|
66
|
+
)
|
|
67
|
+
const hosts = listOption(
|
|
68
|
+
option.hosts ?? parseJson(env.OPENCODE_KEEPALIVE_HOSTS),
|
|
69
|
+
DEFAULTS.hosts,
|
|
70
|
+
)
|
|
71
|
+
const maxRaw = option.maxReplaysPerGap ?? env.OPENCODE_KEEPALIVE_MAX_REPLAYS_PER_GAP
|
|
72
|
+
const maxReplaysPerGap =
|
|
73
|
+
maxRaw === "auto"
|
|
74
|
+
? "auto"
|
|
75
|
+
: positiveNumber(maxRaw) !== undefined
|
|
76
|
+
? positiveNumber(maxRaw)!
|
|
77
|
+
: DEFAULTS.maxReplaysPerGap
|
|
48
78
|
return {
|
|
49
|
-
enabled:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
enabled: boolOption(
|
|
80
|
+
option.enabled,
|
|
81
|
+
env.OPENCODE_KEEPALIVE_ENABLED === undefined
|
|
82
|
+
? DEFAULTS.enabled
|
|
83
|
+
: env.OPENCODE_KEEPALIVE_ENABLED !== "false",
|
|
54
84
|
),
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
85
|
+
intervals,
|
|
86
|
+
hosts,
|
|
87
|
+
cacheReadFactor:
|
|
88
|
+
positiveNumber(option.cacheReadFactor ?? env.OPENCODE_KEEPALIVE_CACHE_READ_FACTOR) ??
|
|
89
|
+
DEFAULTS.cacheReadFactor,
|
|
90
|
+
missFactor:
|
|
91
|
+
positiveNumber(option.missFactor ?? env.OPENCODE_KEEPALIVE_MISS_FACTOR) ??
|
|
92
|
+
DEFAULTS.missFactor,
|
|
93
|
+
maxReplaysPerGap,
|
|
94
|
+
includeChildSessions: boolOption(
|
|
95
|
+
option.includeChildSessions,
|
|
96
|
+
env.OPENCODE_KEEPALIVE_INCLUDE_CHILD_SESSIONS === "true" ||
|
|
97
|
+
DEFAULTS.includeChildSessions,
|
|
59
98
|
),
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
DEFAULTS.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
: DEFAULTS.pingToken,
|
|
68
|
-
revertPing: boolOpt(
|
|
69
|
-
o.revertPing,
|
|
70
|
-
env.OPENCODE_KEEPALIVE_REVERT_PING !== "false" && DEFAULTS.revertPing,
|
|
71
|
-
),
|
|
72
|
-
includeChildSessions: boolOpt(o.includeChildSessions, DEFAULTS.includeChildSessions),
|
|
73
|
-
debug: boolOpt(o.debug, env.OPENCODE_KEEPALIVE_DEBUG === "true"),
|
|
74
|
-
providerAllowlist: listOpt(o.providerAllowlist, DEFAULTS.providerAllowlist),
|
|
75
|
-
modelAllowlist: listOpt(o.modelAllowlist, DEFAULTS.modelAllowlist),
|
|
99
|
+
replayTimeoutMs:
|
|
100
|
+
positiveNumber(option.replayTimeoutMs ?? env.OPENCODE_KEEPALIVE_REPLAY_TIMEOUT_MS) ??
|
|
101
|
+
DEFAULTS.replayTimeoutMs,
|
|
102
|
+
maxStoredBytes:
|
|
103
|
+
positiveNumber(option.maxStoredBytes ?? env.OPENCODE_KEEPALIVE_MAX_STORED_BYTES) ??
|
|
104
|
+
DEFAULTS.maxStoredBytes,
|
|
105
|
+
debug: boolOption(option.debug, env.OPENCODE_KEEPALIVE_DEBUG === "true"),
|
|
76
106
|
}
|
|
77
107
|
}
|
|
78
108
|
|
|
79
|
-
function
|
|
109
|
+
function boolOption(value: unknown, fallback: boolean): boolean {
|
|
80
110
|
return typeof value === "boolean" ? value : fallback
|
|
81
111
|
}
|
|
82
112
|
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return filtered.length > 0 ? filtered : fallback
|
|
113
|
+
function positiveNumber(value: unknown): number | undefined {
|
|
114
|
+
const parsed = typeof value === "string" ? Number(value) : value
|
|
115
|
+
return typeof parsed === "number" && Number.isFinite(parsed) && parsed > 0 ? parsed : undefined
|
|
87
116
|
}
|
|
88
117
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return
|
|
94
|
-
|
|
118
|
+
function objectOption(value: unknown, fallback: Record<string, number>): Record<string, number> {
|
|
119
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return fallback
|
|
120
|
+
const entries = Object.entries(value).flatMap(([key, interval]) => {
|
|
121
|
+
const parsed = positiveNumber(interval)
|
|
122
|
+
return parsed === undefined ? [] : [[key.toLowerCase(), parsed] as const]
|
|
123
|
+
})
|
|
124
|
+
return entries.length ? Object.fromEntries(entries) : fallback
|
|
95
125
|
}
|
|
96
126
|
|
|
97
|
-
function
|
|
98
|
-
if (!
|
|
99
|
-
const
|
|
100
|
-
|
|
127
|
+
function listOption(value: unknown, fallback: string[]): string[] {
|
|
128
|
+
if (!Array.isArray(value)) return fallback
|
|
129
|
+
const values = value.filter(
|
|
130
|
+
(item): item is string => typeof item === "string" && item.length > 0,
|
|
131
|
+
)
|
|
132
|
+
return values.length ? values : fallback
|
|
101
133
|
}
|