@vikrant82/opencode-cache-keepalive 0.1.6 → 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.
Files changed (44) hide show
  1. package/MIGRATION.md +57 -0
  2. package/README.md +75 -51
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1093 -649
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/config.d.ts +8 -25
  7. package/dist/lib/config.d.ts.map +1 -1
  8. package/dist/lib/control.d.ts +10 -37
  9. package/dist/lib/control.d.ts.map +1 -1
  10. package/dist/lib/paths.d.ts +5 -12
  11. package/dist/lib/paths.d.ts.map +1 -1
  12. package/dist/lib/replay-warmer.d.ts +95 -0
  13. package/dist/lib/replay-warmer.d.ts.map +1 -0
  14. package/dist/lib/state.d.ts +15 -117
  15. package/dist/lib/state.d.ts.map +1 -1
  16. package/dist/lib/tui/commands.d.ts +1 -6
  17. package/dist/lib/tui/commands.d.ts.map +1 -1
  18. package/dist/lib/tui/footer.d.ts +5 -5
  19. package/dist/lib/tui/footer.d.ts.map +1 -1
  20. package/dist/lib/tui/format.d.ts.map +1 -1
  21. package/dist/lib/types.d.ts +123 -0
  22. package/dist/lib/types.d.ts.map +1 -0
  23. package/dist/tui.d.ts +3 -0
  24. package/dist/tui.d.ts.map +1 -1
  25. package/lib/config.ts +106 -74
  26. package/lib/control.ts +50 -86
  27. package/lib/paths.ts +12 -14
  28. package/lib/replay-warmer.ts +1200 -0
  29. package/lib/state.ts +171 -181
  30. package/lib/tui/commands.tsx +36 -144
  31. package/lib/tui/footer.tsx +54 -156
  32. package/lib/tui/format.ts +4 -0
  33. package/lib/types.ts +130 -0
  34. package/package.json +3 -2
  35. package/tui.tsx +40 -0
  36. package/dist/lib/keepalive.d.ts +0 -88
  37. package/dist/lib/keepalive.d.ts.map +0 -1
  38. package/dist/lib/model.d.ts +0 -8
  39. package/dist/lib/model.d.ts.map +0 -1
  40. package/dist/lib/system.d.ts +0 -11
  41. package/dist/lib/system.d.ts.map +0 -1
  42. package/lib/keepalive.ts +0 -665
  43. package/lib/model.ts +0 -19
  44. package/lib/system.ts +0 -19
package/lib/state.ts CHANGED
@@ -1,211 +1,201 @@
1
- import { mkdir, rename, writeFile } from "node:fs/promises"
2
- import { dirname } from "node:path"
3
- import type { KeepaliveConfig } from "./config"
4
- import { stateFilePath } from "./paths"
5
-
6
- export type PingRecord = {
7
- at: number
8
- hit: boolean
9
- input: number
10
- cacheRead: number
11
- cacheWrite: number
12
- output: number
13
- }
14
-
15
- /**
16
- * Request settings of the last real user turn. A ping must replay them exactly:
17
- * a prompt without explicit settings runs as opencode's default agent with no
18
- * variant, which changes the request shape (missing the cache) and overwrites
19
- * the session's persisted agent/model/variant.
20
- */
21
- export type PingRequest = {
22
- agent?: string
23
- providerID: string
24
- modelID: string
25
- variant?: string
26
- }
27
-
28
- /** Bookkeeping for the ping request currently in flight. */
29
- export type InflightPing = {
30
- /** Epoch ms the ping attempt began (before the pre-flight status check). */
31
- startedAt: number
32
- /** The prompt request has been issued. */
33
- sent: boolean
34
- /** The ping's own user message, identified by its ping-token text part. */
35
- messageID?: string
36
- /**
37
- * New user messages seen during the ping whose text has not yet been observed.
38
- * Each is classified by its first text part: the ping token marks our own
39
- * message, anything else a real turn.
40
- */
41
- pending: Map<string, unknown>
42
- /**
43
- * A real user message appeared while the ping was in flight. opencode joins such
44
- * a prompt to the running ping loop, so the rest of the run is real work.
45
- */
46
- interrupted: boolean
47
- }
48
-
49
- /** Live per-session bookkeeping held by the server plugin. */
50
- export type SessionKeepalive = {
51
- sessionID: string
52
- /** Whether the session's provider/model pair supports cache warming. */
53
- eligible: boolean
54
- modelLabel?: string
55
- /** Epoch ms of the last real assistant response (excludes ping replies). */
56
- lastResponseAt: number
57
- /** Epoch ms the current idle stretch began. */
58
- idleSince: number
59
- /** Epoch ms after which warming stops and the cache is allowed to go cold. */
60
- windowEndsAt: number
61
- /** Epoch ms the next ping is due. */
62
- nextPingAt: number
63
- /** Number of scheduled ping requests attempted during this warm window. */
64
- pingsSent: number
65
- /** A real turn is currently running. */
66
- busy: boolean
67
- /** A keepalive ping is currently in flight. */
68
- warming: boolean
69
- /** The in-flight ping; set exactly while `warming` is true. */
70
- ping?: InflightPing
71
- /** ID and creation time of the newest user message seen, to tell new turns from re-emitted old messages. */
72
- lastUserID?: string
73
- lastUserAt?: number
74
- /** Settings of the last real user turn, replayed by pings. */
75
- request?: PingRequest
76
- /** Within the warm window and actively scheduling pings. */
77
- active: boolean
78
- /** True while `armWindow` is resolving session metadata (async guard). */
79
- arming?: boolean
80
- /** Epoch ms the last sent ping finished; ignores its late events and marks the last cache touch. */
81
- lastPingAt?: number
82
- lastPing?: PingRecord
83
- }
84
-
85
- /** Lean projection persisted to disk for the TUI reader. */
86
- export type PersistedSession = {
87
- eligible: boolean
88
- modelLabel?: string
89
- lastResponseAt: number
90
- idleSince: number
91
- windowEndsAt: number
92
- nextPingAt: number
93
- intervalMs: number
94
- pingsSent: number
95
- busy: boolean
96
- warming: boolean
97
- active: boolean
98
- lastPing?: PingRecord
99
- }
100
-
101
- export type PersistedState = {
102
- version: 1
1
+ import { randomUUID } from "node:crypto"
2
+ import { readdirSync, readFileSync, unlinkSync } from "node:fs"
3
+ import { mkdir, rename, unlink, writeFile } from "node:fs/promises"
4
+ import { dirname, join } from "node:path"
5
+ import type { Logger } from "./logger"
6
+ import { instanceStateFilePath, stateDirectoryKey, stateDirectoryPath } from "./paths"
7
+ import type { ProcessTotals, SessionWarmEntry, WarmStateSink } from "./types"
8
+
9
+ const CLEANUP_INTERVAL_MS = 10 * 60_000
10
+ const PERSIST_DEBOUNCE_MS = 250
11
+
12
+ type StateFile = {
13
+ version: 2
14
+ pid: number
15
+ instanceId: string
103
16
  updatedAt: number
104
- enabled: boolean
105
- intervalMs: number
106
- windowMs: number
107
- sessions: Record<string, PersistedSession>
17
+ sessions: Record<string, SessionWarmEntry>
18
+ totals: ProcessTotals
108
19
  }
109
20
 
110
- export class KeepaliveStore {
111
- private readonly sessions = new Map<string, SessionKeepalive>()
112
- private writeQueue: Promise<void> = Promise.resolve()
113
- private enabled: boolean
114
- private intervalMs: number
21
+ export type StateStoreOptions = { logger?: Logger; now?: () => number }
115
22
 
116
- constructor(
117
- private readonly config: KeepaliveConfig,
118
- private readonly directory: string,
119
- ) {
120
- this.enabled = config.enabled
121
- this.intervalMs = config.intervalMs
23
+ /**
24
+ * Create an isolated v2 snapshot writer. Mutations are debounced, dispose flushes
25
+ * the latest snapshot before removing it, and all filesystem failures stay
26
+ * best-effort so state reporting cannot disrupt replay execution.
27
+ */
28
+ export function createStateStore(
29
+ directory: string,
30
+ options: StateStoreOptions = {},
31
+ ): WarmStateSink {
32
+ const now = options.now ?? Date.now
33
+ const instanceId = randomUUID()
34
+ const filePath = instanceStateFilePath(directory, process.pid, instanceId)
35
+ const sessions = new Map<string, SessionWarmEntry>()
36
+ let totals: ProcessTotals = {
37
+ replays: 0,
38
+ avoidedTokens: 0,
39
+ replayReadTokens: 0,
40
+ resumeHits: 0,
41
+ resumeMisses: 0,
122
42
  }
123
-
124
- setEnabled(enabled: boolean): void {
125
- this.enabled = enabled
43
+ let timer: ReturnType<typeof setTimeout> | undefined
44
+ let cleanupTimer: ReturnType<typeof setInterval> | undefined
45
+ let writeQueue: Promise<void> = Promise.resolve()
46
+ let disposed = false
47
+
48
+ const flush = (): Promise<void> => {
49
+ if (timer) clearTimeout(timer)
50
+ timer = undefined
51
+ const snapshot: StateFile = {
52
+ version: 2,
53
+ pid: process.pid,
54
+ instanceId,
55
+ updatedAt: now(),
56
+ sessions: Object.fromEntries(sessions),
57
+ totals,
58
+ }
59
+ const write = () => atomicWrite(filePath, JSON.stringify(snapshot))
60
+ writeQueue = writeQueue.then(write, write)
61
+ return writeQueue
126
62
  }
127
63
 
128
- /** Effective ping interval (runtime override or config) reported to the TUI. */
129
- setIntervalMs(intervalMs: number): void {
130
- this.intervalMs = intervalMs
64
+ const schedule = () => {
65
+ if (disposed || timer) return
66
+ timer = setTimeout(() => {
67
+ timer = undefined
68
+ void flush()
69
+ }, PERSIST_DEBOUNCE_MS)
131
70
  }
132
71
 
133
- get(sessionID: string): SessionKeepalive | undefined {
134
- return this.sessions.get(sessionID)
72
+ const onExit = () => {
73
+ try {
74
+ unlinkSync(filePath)
75
+ } catch {
76
+ // Exit cleanup is best-effort.
77
+ }
135
78
  }
136
79
 
137
- ensure(sessionID: string): SessionKeepalive {
138
- const existing = this.sessions.get(sessionID)
139
- if (existing) return existing
140
- const created: SessionKeepalive = {
141
- sessionID,
142
- eligible: false,
143
- lastResponseAt: 0,
144
- idleSince: 0,
145
- windowEndsAt: 0,
146
- nextPingAt: 0,
147
- pingsSent: 0,
148
- busy: false,
149
- warming: false,
150
- active: false,
151
- }
152
- this.sessions.set(sessionID, created)
153
- return created
80
+ cleanupStateFiles(directory, options.logger)
81
+ cleanupTimer = setInterval(
82
+ () => cleanupStateFiles(directory, options.logger),
83
+ CLEANUP_INTERVAL_MS,
84
+ )
85
+ process.on("exit", onExit)
86
+
87
+ return {
88
+ upsert(entry) {
89
+ if (disposed) return
90
+ sessions.set(entry.sessionID, { ...entry })
91
+ schedule()
92
+ },
93
+ remove(sessionID) {
94
+ if (disposed) return
95
+ sessions.delete(sessionID)
96
+ schedule()
97
+ },
98
+ setTotals(value) {
99
+ if (disposed) return
100
+ totals = { ...value }
101
+ schedule()
102
+ },
103
+ async dispose() {
104
+ if (disposed) return
105
+ disposed = true
106
+ if (timer) clearTimeout(timer)
107
+ timer = undefined
108
+ if (cleanupTimer) clearInterval(cleanupTimer)
109
+ cleanupTimer = undefined
110
+ process.off("exit", onExit)
111
+ await flush()
112
+ await unlink(filePath).catch(() => {})
113
+ },
154
114
  }
115
+ }
155
116
 
156
- remove(sessionID: string): void {
157
- this.sessions.delete(sessionID)
117
+ /** Return the newest self-contained entry for a session across matching v2 files. */
118
+ export function readSessionEntry(
119
+ directory: string,
120
+ sessionID: string,
121
+ ): SessionWarmEntry | undefined {
122
+ let freshest: SessionWarmEntry | undefined
123
+ for (const snapshot of readSnapshots(directory)) {
124
+ const entry = snapshot.sessions[sessionID]
125
+ if (entry && (!freshest || entry.updatedAt > freshest.updatedAt)) freshest = entry
158
126
  }
127
+ return freshest
128
+ }
159
129
 
160
- all(): SessionKeepalive[] {
161
- return [...this.sessions.values()]
130
+ /** Sum process-level totals from live snapshots for this directory. */
131
+ export function readProcessTotals(directory: string): ProcessTotals {
132
+ const totals = {
133
+ replays: 0,
134
+ avoidedTokens: 0,
135
+ replayReadTokens: 0,
136
+ resumeHits: 0,
137
+ resumeMisses: 0,
138
+ }
139
+ for (const snapshot of readSnapshots(directory)) {
140
+ totals.replays += snapshot.totals.replays
141
+ totals.avoidedTokens += snapshot.totals.avoidedTokens
142
+ totals.replayReadTokens += snapshot.totals.replayReadTokens
143
+ totals.resumeHits += snapshot.totals.resumeHits
144
+ totals.resumeMisses += snapshot.totals.resumeMisses
162
145
  }
146
+ return totals
147
+ }
163
148
 
164
- /**
165
- * Atomically persist a lean snapshot for the TUI. Never throws or rejects; the
166
- * returned promise resolves once this snapshot and every earlier one are written.
167
- */
168
- persist(): Promise<void> {
169
- const snapshot: PersistedState = {
170
- version: 1,
171
- updatedAt: Date.now(),
172
- enabled: this.enabled,
173
- intervalMs: this.intervalMs,
174
- windowMs: this.config.windowMs,
175
- sessions: Object.fromEntries(
176
- this.all().map((s) => [
177
- s.sessionID,
178
- {
179
- eligible: s.eligible,
180
- modelLabel: s.modelLabel,
181
- lastResponseAt: s.lastResponseAt,
182
- idleSince: s.idleSince,
183
- windowEndsAt: s.windowEndsAt,
184
- nextPingAt: s.nextPingAt,
185
- intervalMs: this.intervalMs,
186
- pingsSent: s.pingsSent,
187
- busy: s.busy,
188
- warming: s.warming,
189
- active: s.active,
190
- lastPing: s.lastPing,
191
- } satisfies PersistedSession,
192
- ]),
193
- ),
149
+ /** Remove only v2 snapshots whose owner PID is confirmed dead. */
150
+ export function cleanupStateFiles(directory: string, logger?: Logger): void {
151
+ try {
152
+ const prefix = `state-v2-${stateDirectoryKey(directory)}-`
153
+ for (const name of readdirSync(stateDirectoryPath())) {
154
+ const match = name.match(new RegExp(`^${prefix}(\\d+)-[^/]+\\.json$`))
155
+ if (!match) continue
156
+ const pid = Number(match[1])
157
+ try {
158
+ process.kill(pid, 0)
159
+ } catch (error) {
160
+ if ((error as NodeJS.ErrnoException).code === "ESRCH") {
161
+ unlinkSync(join(stateDirectoryPath(), name))
162
+ logger?.dbg("Removed stale state snapshot", { pid })
163
+ }
164
+ }
194
165
  }
166
+ } catch {
167
+ // Storage can be unavailable during startup/shutdown; cleanup is optional.
168
+ }
169
+ }
195
170
 
196
- const write = () => atomicWrite(stateFilePath(this.directory), JSON.stringify(snapshot))
197
- this.writeQueue = this.writeQueue.then(write, write)
198
- return this.writeQueue
171
+ function readSnapshots(directory: string): StateFile[] {
172
+ const prefix = `state-v2-${stateDirectoryKey(directory)}-`
173
+ try {
174
+ return readdirSync(stateDirectoryPath())
175
+ .filter((name) => name.startsWith(prefix) && name.endsWith(".json"))
176
+ .map((name) => {
177
+ try {
178
+ const value = JSON.parse(
179
+ readFileSync(join(stateDirectoryPath(), name), "utf8"),
180
+ ) as StateFile
181
+ return value.version === 2 && value.sessions && value.totals ? value : undefined
182
+ } catch {
183
+ return undefined
184
+ }
185
+ })
186
+ .filter((value): value is StateFile => value !== undefined)
187
+ } catch {
188
+ return []
199
189
  }
200
190
  }
201
191
 
202
192
  async function atomicWrite(path: string, contents: string): Promise<void> {
203
193
  try {
204
- const tmp = `${path}.${process.pid}.${Date.now()}.tmp`
194
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`
205
195
  await mkdir(dirname(path), { recursive: true })
206
196
  await writeFile(tmp, `${contents}\n`, "utf8")
207
197
  await rename(tmp, path)
208
198
  } catch {
209
- // Persistence is best-effort; the TUI readout simply falls behind.
199
+ // Best-effort persistence; stale display data is safer than disrupting replay.
210
200
  }
211
201
  }
@@ -1,184 +1,76 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
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"
3
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
4
+ import { readSessionControl, setSessionEnabled } from "../control"
15
5
 
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
- */
6
+ /** Register runtime enable/disable commands for the current session only. */
30
7
  export function registerKeepaliveCommands(api: TuiPluginApi): void {
31
- // Read lazily: state paths may not be populated at registration time.
32
8
  const directory = () => api.state.path.directory
33
-
34
- const apply = async (
35
- patch: ControlPatch,
36
- message: string,
37
- variant: "success" | "warning" | "info",
38
- ) => {
9
+ const sessionID = () => {
10
+ const route = api.route.current
11
+ return route.name === "session" && route.params && "sessionID" in route.params
12
+ ? String(route.params.sessionID)
13
+ : undefined
14
+ }
15
+ const setEnabled = async (enabled: boolean, id: string) => {
39
16
  try {
40
- await updateControl(directory(), patch)
41
- api.ui.toast({ variant, title: "Cache keepalive", message })
17
+ await setSessionEnabled(directory(), id, enabled)
18
+ api.ui.toast({
19
+ variant: enabled ? "success" : "warning",
20
+ title: "Keepalive",
21
+ message: enabled
22
+ ? "Keepalive on for this session"
23
+ : "Keepalive off for this session",
24
+ })
42
25
  } catch (error) {
43
26
  api.ui.toast({
44
27
  variant: "error",
45
- title: "Cache keepalive",
28
+ title: "Keepalive",
46
29
  message: error instanceof Error ? error.message : String(error),
47
30
  })
48
31
  }
49
32
  }
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",
33
+ const run = (operation: (id: string) => void | Promise<void>) => () => {
34
+ const id = sessionID()
35
+ if (!id) {
36
+ api.ui.toast({
37
+ variant: "warning",
38
+ title: "Keepalive",
39
+ message: "Open a session to toggle keepalive",
99
40
  })
41
+ return
100
42
  }
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
- ))
43
+ return operation(id)
130
44
  }
131
45
 
132
46
  api.keymap.registerLayer({
133
47
  commands: [
134
48
  {
135
49
  name: "keepalive.toggle",
136
- title: "Toggle cache keepalive",
50
+ title: "Toggle replay cache warmer",
137
51
  category: "Keepalive",
138
52
  namespace: "palette",
139
53
  slashName: "keepalive-toggle",
140
- run: () => setEnabled(!currentEnabled(directory())),
54
+ run: run((id) =>
55
+ setEnabled(!(readSessionControl(directory(), id)?.enabled ?? true), id),
56
+ ),
141
57
  },
142
58
  {
143
59
  name: "keepalive.on",
144
- title: "Enable cache keepalive",
60
+ title: "Enable replay cache warmer",
145
61
  category: "Keepalive",
146
62
  namespace: "palette",
147
63
  slashName: "keepalive-on",
148
- run: () => setEnabled(true),
64
+ run: run((id) => setEnabled(true, id)),
149
65
  },
150
66
  {
151
67
  name: "keepalive.off",
152
- title: "Disable cache keepalive",
68
+ title: "Disable replay cache warmer",
153
69
  category: "Keepalive",
154
70
  namespace: "palette",
155
71
  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,
72
+ run: run((id) => setEnabled(false, id)),
165
73
  },
166
74
  ],
167
75
  })
168
76
  }
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
- }