@miphamai/cli 0.31.1 → 0.32.1
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/bin/daemon.ts +40 -0
- package/bin/mipham.ts +796 -1
- package/package.json +1 -1
- package/src/agent/background-registry.ts +80 -1
- package/src/daemon/agent-manager.ts +110 -0
- package/src/daemon/attach-protocol.ts +74 -0
- package/src/daemon/auth.ts +104 -0
- package/src/daemon/cors.ts +68 -0
- package/src/daemon/database.ts +449 -0
- package/src/daemon/goal-manager.ts +53 -0
- package/src/daemon/index.ts +307 -0
- package/src/daemon/message-bus.ts +84 -0
- package/src/daemon/rate-limiter.ts +70 -0
- package/src/daemon/remote-engine.ts +434 -0
- package/src/daemon/schedule-manager.ts +159 -0
- package/src/daemon/server.ts +632 -0
- package/src/daemon/session-manager.ts +45 -0
- package/src/daemon/session-worker.ts +418 -0
- package/src/daemon/types.ts +98 -0
- package/src/daemon/worker-pool.ts +174 -0
- package/src/index.tsx +44 -4
- package/src/ui/app.tsx +6 -2
- package/src/ui/chat.tsx +39 -33
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// apps/cli/src/daemon/index.ts — Daemon Lifecycle Management
|
|
2
|
+
//
|
|
3
|
+
// startDaemon() initializes DB + SessionManager + HTTP server, writes PID/port files
|
|
4
|
+
// stopDaemon() shuts down server, closes DB, cleans up PID/port files
|
|
5
|
+
// getDaemonStatus() reads PID file and verifies process is alive
|
|
6
|
+
// getPort() reads the last known port from disk
|
|
7
|
+
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs'
|
|
10
|
+
import { createServer as createNetServer } from 'node:net'
|
|
11
|
+
import { homedir } from 'node:os'
|
|
12
|
+
import type { Server } from 'bun'
|
|
13
|
+
import { DaemonDatabase } from './database'
|
|
14
|
+
import { SessionManager } from './session-manager'
|
|
15
|
+
import { AgentManager } from './agent-manager'
|
|
16
|
+
import { MessageBus } from './message-bus'
|
|
17
|
+
import { GoalManager } from './goal-manager'
|
|
18
|
+
import { ScheduleManager } from './schedule-manager'
|
|
19
|
+
import { createServer } from './server'
|
|
20
|
+
import { WorkerPool } from './worker-pool'
|
|
21
|
+
import { loadOrCreateToken } from './auth'
|
|
22
|
+
import { RateLimiter } from './rate-limiter'
|
|
23
|
+
import { PACKAGE_VERSION } from '../shared/package-info'
|
|
24
|
+
import type { DaemonStatus } from './types'
|
|
25
|
+
|
|
26
|
+
const HOME = homedir()
|
|
27
|
+
const MIPHAM_HOME = join(HOME, '.mipham')
|
|
28
|
+
const DB_PATH = join(MIPHAM_HOME, 'daemon.db')
|
|
29
|
+
const TOKEN_PATH = join(MIPHAM_HOME, 'daemon.token')
|
|
30
|
+
const PID_FILE = join(MIPHAM_HOME, 'daemon.pid')
|
|
31
|
+
const PORT_FILE = join(MIPHAM_HOME, 'daemon.port')
|
|
32
|
+
|
|
33
|
+
const DEFAULT_PORT = 45671
|
|
34
|
+
|
|
35
|
+
// The lifecycle only needs .stop() on the server; WebSocketData type is opaque here.
|
|
36
|
+
let activeServer: Server<unknown> | null = null
|
|
37
|
+
let activeDb: DaemonDatabase | null = null
|
|
38
|
+
let activePool: WorkerPool | null = null
|
|
39
|
+
let activeAgentManager: AgentManager | null = null
|
|
40
|
+
let activeMessageBus: MessageBus | null = null
|
|
41
|
+
let activeScheduleManager: ScheduleManager | null = null
|
|
42
|
+
let activeRateLimiter: RateLimiter | null = null
|
|
43
|
+
|
|
44
|
+
function getConfiguredPort(): number {
|
|
45
|
+
if (process.env.MIPHAM_PORT) {
|
|
46
|
+
const p = parseInt(process.env.MIPHAM_PORT, 10)
|
|
47
|
+
if (!isNaN(p) && p > 0 && p < 65536) return p
|
|
48
|
+
}
|
|
49
|
+
return DEFAULT_PORT
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getConfiguredBind(): string {
|
|
53
|
+
return process.env.MIPHAM_BIND || '127.0.0.1'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Test whether a TCP port is available on the given hostname.
|
|
58
|
+
*/
|
|
59
|
+
function isPortAvailable(port: number, hostname: string): Promise<boolean> {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const server = createNetServer()
|
|
62
|
+
server.once('error', () => {
|
|
63
|
+
server.removeAllListeners()
|
|
64
|
+
resolve(false)
|
|
65
|
+
})
|
|
66
|
+
server.once('listening', () => {
|
|
67
|
+
server.close()
|
|
68
|
+
resolve(true)
|
|
69
|
+
})
|
|
70
|
+
server.listen(port, hostname)
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Find an available port starting from startPort, trying up to 10 ports.
|
|
76
|
+
* Falls back to startPort if the range is exhausted (Bun.serve will throw if
|
|
77
|
+
* the port is actually unavailable at bind time).
|
|
78
|
+
*/
|
|
79
|
+
async function findAvailablePort(startPort: number, hostname: string): Promise<number> {
|
|
80
|
+
for (let port = startPort; port < startPort + 10; port++) {
|
|
81
|
+
if (await isPortAvailable(port, hostname)) {
|
|
82
|
+
return port
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// If all ports in range appear busy, return startPort and let Bun.serve fail
|
|
86
|
+
// with a clear error rather than silently binding to an unexpected port.
|
|
87
|
+
return startPort
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read the last daemon port from the port file on disk.
|
|
92
|
+
* Returns DEFAULT_PORT if the file doesn't exist or is unreadable.
|
|
93
|
+
*/
|
|
94
|
+
export function getPort(): number {
|
|
95
|
+
if (existsSync(PORT_FILE)) {
|
|
96
|
+
try {
|
|
97
|
+
const raw = readFileSync(PORT_FILE, 'utf-8').trim()
|
|
98
|
+
const port = parseInt(raw, 10)
|
|
99
|
+
if (!isNaN(port) && port > 0 && port < 65536) return port
|
|
100
|
+
} catch {
|
|
101
|
+
// Corrupt port file — fall through to default
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return DEFAULT_PORT
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Start the Mipham Code daemon.
|
|
109
|
+
*
|
|
110
|
+
* 1. Ensures ~/.mipham exists (mode 0o700)
|
|
111
|
+
* 2. Loads or creates the auth token
|
|
112
|
+
* 3. Initializes the SQLite database and runs JSONL migration on first start
|
|
113
|
+
* 4. Creates a SessionManager, AgentManager, and MessageBus
|
|
114
|
+
* 5. Starts the HTTP server on an available port
|
|
115
|
+
* 6. Writes PID and port files to disk
|
|
116
|
+
*
|
|
117
|
+
* Returns the port and token so the CLI can connect immediately.
|
|
118
|
+
*/
|
|
119
|
+
export async function startDaemon(): Promise<{ port: number; token: string }> {
|
|
120
|
+
mkdirSync(MIPHAM_HOME, { recursive: true, mode: 0o700 })
|
|
121
|
+
|
|
122
|
+
const hostname = getConfiguredBind()
|
|
123
|
+
const port = await findAvailablePort(getConfiguredPort(), hostname)
|
|
124
|
+
const token = loadOrCreateToken(TOKEN_PATH)
|
|
125
|
+
|
|
126
|
+
// Initialize database
|
|
127
|
+
const db = new DaemonDatabase(DB_PATH)
|
|
128
|
+
db.init()
|
|
129
|
+
activeDb = db
|
|
130
|
+
|
|
131
|
+
// Run JSONL migration on first start (when no sessions exist yet)
|
|
132
|
+
const sessionsCount = db.listSessions().length
|
|
133
|
+
if (sessionsCount === 0) {
|
|
134
|
+
const migrated = db.migrateFromJsonl()
|
|
135
|
+
if (migrated > 0) {
|
|
136
|
+
console.log(`Daemon: migrated ${migrated} JSONL session(s) to SQLite`)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Create session manager
|
|
141
|
+
const sm = new SessionManager(db)
|
|
142
|
+
|
|
143
|
+
// Create worker pool (idle timeout: 30 min default)
|
|
144
|
+
const pool = new WorkerPool(db)
|
|
145
|
+
activePool = pool
|
|
146
|
+
|
|
147
|
+
// Create agent manager and message bus (Phase 3)
|
|
148
|
+
const agentManager = new AgentManager(db)
|
|
149
|
+
activeAgentManager = agentManager
|
|
150
|
+
const messageBus = new MessageBus()
|
|
151
|
+
activeMessageBus = messageBus
|
|
152
|
+
|
|
153
|
+
// Create goal manager and schedule manager (Phase 4)
|
|
154
|
+
const goalManager = new GoalManager(db)
|
|
155
|
+
const scheduleManager = new ScheduleManager(db, pool)
|
|
156
|
+
activeScheduleManager = scheduleManager
|
|
157
|
+
|
|
158
|
+
// Start the schedule checking interval (checks every 60s)
|
|
159
|
+
scheduleManager.start()
|
|
160
|
+
|
|
161
|
+
// Create rate limiter (Phase 5 — 100 req/min per client IP)
|
|
162
|
+
const rateLimiter = new RateLimiter(100, 60_000)
|
|
163
|
+
activeRateLimiter = rateLimiter
|
|
164
|
+
|
|
165
|
+
// Start HTTP server (Bun.serve starts listening immediately)
|
|
166
|
+
const server = createServer({
|
|
167
|
+
db,
|
|
168
|
+
sm,
|
|
169
|
+
pool,
|
|
170
|
+
token,
|
|
171
|
+
tokenPath: TOKEN_PATH,
|
|
172
|
+
port,
|
|
173
|
+
hostname,
|
|
174
|
+
agentManager,
|
|
175
|
+
messageBus,
|
|
176
|
+
goalManager,
|
|
177
|
+
scheduleManager,
|
|
178
|
+
rateLimiter,
|
|
179
|
+
})
|
|
180
|
+
activeServer = server
|
|
181
|
+
|
|
182
|
+
// Write PID and port files
|
|
183
|
+
writeFileSync(PID_FILE, String(process.pid))
|
|
184
|
+
writeFileSync(PORT_FILE, String(port))
|
|
185
|
+
|
|
186
|
+
return { port, token }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Stop the Mipham Code daemon.
|
|
191
|
+
*
|
|
192
|
+
* By default, refuses to stop if there are active sessions.
|
|
193
|
+
* Pass `force: true` to bypass this check (active sessions will be
|
|
194
|
+
* disconnected when the server stops).
|
|
195
|
+
*
|
|
196
|
+
* Cleans up PID and port files regardless of success/failure.
|
|
197
|
+
*/
|
|
198
|
+
export async function stopDaemon(force: boolean = false): Promise<void> {
|
|
199
|
+
// Guard: check for active sessions unless forced
|
|
200
|
+
if (!force && activeDb) {
|
|
201
|
+
const stats = activeDb.getStats()
|
|
202
|
+
if (stats.activeSessions > 0) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Cannot stop: ${stats.activeSessions} active session(s). Use --force to stop anyway.`,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Stop HTTP server (disconnects WebSocket clients)
|
|
210
|
+
if (activeServer) {
|
|
211
|
+
activeServer.stop()
|
|
212
|
+
activeServer = null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Stop worker pool (interrupts all in-progress prompts, persists state)
|
|
216
|
+
if (activePool) {
|
|
217
|
+
await activePool.stopAll()
|
|
218
|
+
activePool = null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Stop schedule manager interval (Phase 4)
|
|
222
|
+
if (activeScheduleManager) {
|
|
223
|
+
activeScheduleManager.stop()
|
|
224
|
+
activeScheduleManager = null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Stop rate limiter cleanup interval (Phase 5)
|
|
228
|
+
if (activeRateLimiter) {
|
|
229
|
+
activeRateLimiter.stop()
|
|
230
|
+
activeRateLimiter = null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Clean up agent manager and message bus (Phase 3)
|
|
234
|
+
activeAgentManager = null
|
|
235
|
+
activeMessageBus = null
|
|
236
|
+
|
|
237
|
+
// Close database (commits any pending WAL)
|
|
238
|
+
if (activeDb) {
|
|
239
|
+
activeDb.close()
|
|
240
|
+
activeDb = null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Clean up PID/port files — best-effort, ignore errors
|
|
244
|
+
try {
|
|
245
|
+
unlinkSync(PID_FILE)
|
|
246
|
+
} catch {
|
|
247
|
+
// File may not exist
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
unlinkSync(PORT_FILE)
|
|
251
|
+
} catch {
|
|
252
|
+
// File may not exist
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Check whether a daemon process is currently running.
|
|
258
|
+
*
|
|
259
|
+
* Returns DaemonStatus with basic info if the daemon is alive (PID exists
|
|
260
|
+
* and process is reachable), or null if no daemon is running.
|
|
261
|
+
*
|
|
262
|
+
* Fields like uptime, activeSessions, etc. are filled with 0 when queried
|
|
263
|
+
* from an external process. For real-time stats, call GET /api/v1/health
|
|
264
|
+
* directly against the running daemon.
|
|
265
|
+
*/
|
|
266
|
+
export function getDaemonStatus(): DaemonStatus | null {
|
|
267
|
+
if (!existsSync(PID_FILE)) return null
|
|
268
|
+
|
|
269
|
+
let pid = 0
|
|
270
|
+
try {
|
|
271
|
+
const raw = readFileSync(PID_FILE, 'utf-8').trim()
|
|
272
|
+
pid = parseInt(raw, 10)
|
|
273
|
+
if (isNaN(pid) || pid <= 0) return null
|
|
274
|
+
} catch {
|
|
275
|
+
return null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Signal 0 checks process existence without actually sending a signal
|
|
279
|
+
try {
|
|
280
|
+
process.kill(pid, 0)
|
|
281
|
+
} catch {
|
|
282
|
+
// Stale PID file — process is gone, clean up
|
|
283
|
+
try {
|
|
284
|
+
unlinkSync(PID_FILE)
|
|
285
|
+
} catch {
|
|
286
|
+
// Best effort
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
unlinkSync(PORT_FILE)
|
|
290
|
+
} catch {
|
|
291
|
+
// Best effort
|
|
292
|
+
}
|
|
293
|
+
return null
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const port = getPort()
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
pid,
|
|
300
|
+
port,
|
|
301
|
+
uptime: 0, // Call GET /api/v1/health for real-time stats
|
|
302
|
+
activeSessions: 0,
|
|
303
|
+
totalSessions: 0,
|
|
304
|
+
activeAgents: 0,
|
|
305
|
+
version: PACKAGE_VERSION,
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// apps/cli/src/daemon/message-bus.ts — Phase 3: Inter-Agent Message Bus
|
|
2
|
+
//
|
|
3
|
+
// MessageBus provides asynchronous message passing between agents:
|
|
4
|
+
// - send() — enqueue a message from one agent to another
|
|
5
|
+
// - poll() — retrieve and clear pending messages (discards TTL-expired)
|
|
6
|
+
// - broadcastToSession() — send a message to all agents in a session
|
|
7
|
+
//
|
|
8
|
+
// Messages older than 5 minutes are automatically discarded on poll.
|
|
9
|
+
// Session-to-agent mapping is maintained externally via registerAgent/unregisterAgent.
|
|
10
|
+
|
|
11
|
+
interface BusMessage {
|
|
12
|
+
sender: string
|
|
13
|
+
content: string
|
|
14
|
+
timestamp: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class MessageBus {
|
|
18
|
+
private queues = new Map<string, BusMessage[]>()
|
|
19
|
+
private sessionAgents = new Map<string, Set<string>>()
|
|
20
|
+
private readonly ttlMs = 5 * 60 * 1000 // 5 minutes
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register an agent to a session so that broadcastToSession can reach it.
|
|
24
|
+
*/
|
|
25
|
+
registerAgent(sessionId: string, agentId: string): void {
|
|
26
|
+
if (!this.sessionAgents.has(sessionId)) {
|
|
27
|
+
this.sessionAgents.set(sessionId, new Set())
|
|
28
|
+
}
|
|
29
|
+
this.sessionAgents.get(sessionId)!.add(agentId)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Remove an agent from all session registrations and clear its message queue.
|
|
34
|
+
*/
|
|
35
|
+
unregisterAgent(agentId: string): void {
|
|
36
|
+
for (const [, agents] of this.sessionAgents) {
|
|
37
|
+
agents.delete(agentId)
|
|
38
|
+
}
|
|
39
|
+
this.queues.delete(agentId)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Enqueue a message from senderAgentId to receiverAgentId.
|
|
44
|
+
*/
|
|
45
|
+
send(senderAgentId: string, receiverAgentId: string, content: string): void {
|
|
46
|
+
if (!this.queues.has(receiverAgentId)) {
|
|
47
|
+
this.queues.set(receiverAgentId, [])
|
|
48
|
+
}
|
|
49
|
+
this.queues.get(receiverAgentId)!.push({
|
|
50
|
+
sender: senderAgentId,
|
|
51
|
+
content,
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Retrieve and clear all pending messages for an agent.
|
|
58
|
+
* Messages older than the TTL (5 min) are discarded.
|
|
59
|
+
*/
|
|
60
|
+
poll(agentId: string): BusMessage[] {
|
|
61
|
+
const queue = this.queues.get(agentId)
|
|
62
|
+
if (!queue || queue.length === 0) return []
|
|
63
|
+
|
|
64
|
+
const now = Date.now()
|
|
65
|
+
const valid = queue.filter((msg) => now - msg.timestamp < this.ttlMs)
|
|
66
|
+
|
|
67
|
+
this.queues.delete(agentId)
|
|
68
|
+
return valid
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Send a message to every agent in a session except the one specified.
|
|
73
|
+
* The sender is recorded as 'system'.
|
|
74
|
+
*/
|
|
75
|
+
broadcastToSession(sessionId: string, content: string, excludeAgentId?: string): void {
|
|
76
|
+
const agents = this.sessionAgents.get(sessionId)
|
|
77
|
+
if (!agents) return
|
|
78
|
+
for (const agentId of agents) {
|
|
79
|
+
if (agentId !== excludeAgentId) {
|
|
80
|
+
this.send('system', agentId, content)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// apps/cli/src/daemon/rate-limiter.ts
|
|
2
|
+
//
|
|
3
|
+
// In-memory sliding-window rate limiter for daemon HTTP API.
|
|
4
|
+
// Each key (typically client IP or token) gets maxRequests per windowMs.
|
|
5
|
+
|
|
6
|
+
interface WindowEntry {
|
|
7
|
+
count: number
|
|
8
|
+
resetAt: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class RateLimiter {
|
|
12
|
+
private windows = new Map<string, WindowEntry>()
|
|
13
|
+
private cleanupTimer: ReturnType<typeof setInterval>
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
private maxRequests: number = 100,
|
|
17
|
+
private windowMs: number = 60_000,
|
|
18
|
+
) {
|
|
19
|
+
// Clean expired entries every 60 seconds
|
|
20
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), 60_000)
|
|
21
|
+
// Allow timer to not block process exit
|
|
22
|
+
if (typeof this.cleanupTimer === 'object' && 'unref' in this.cleanupTimer) {
|
|
23
|
+
;(this.cleanupTimer as unknown as { unref(): void }).unref()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check whether a request from `key` should be allowed.
|
|
29
|
+
* Returns the decision along with remaining quota and reset timestamp.
|
|
30
|
+
*/
|
|
31
|
+
check(key: string): { allowed: boolean; remaining: number; resetAt: number } {
|
|
32
|
+
const now = Date.now()
|
|
33
|
+
let entry = this.windows.get(key)
|
|
34
|
+
|
|
35
|
+
if (!entry || now >= entry.resetAt) {
|
|
36
|
+
entry = { count: 0, resetAt: now + this.windowMs }
|
|
37
|
+
this.windows.set(key, entry)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (entry.count >= this.maxRequests) {
|
|
41
|
+
return { allowed: false, remaining: 0, resetAt: entry.resetAt }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
entry.count++
|
|
45
|
+
return {
|
|
46
|
+
allowed: true,
|
|
47
|
+
remaining: this.maxRequests - entry.count,
|
|
48
|
+
resetAt: entry.resetAt,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Remove all entries whose window has expired.
|
|
54
|
+
*/
|
|
55
|
+
private cleanup(): void {
|
|
56
|
+
const now = Date.now()
|
|
57
|
+
for (const [key, entry] of this.windows) {
|
|
58
|
+
if (now >= entry.resetAt) {
|
|
59
|
+
this.windows.delete(key)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Stop the cleanup interval. Call when shutting down the daemon.
|
|
66
|
+
*/
|
|
67
|
+
stop(): void {
|
|
68
|
+
clearInterval(this.cleanupTimer)
|
|
69
|
+
}
|
|
70
|
+
}
|