blun-king-cli 9.1.474 → 9.1.476

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.
@@ -228,6 +228,9 @@ function createAgentApiHttpAdapter(options = {}) {
228
228
  if (typeof options.closeSession !== 'function') {
229
229
  throw new TypeError('closeSession must be a function');
230
230
  }
231
+ if (options.touchSession !== undefined && typeof options.touchSession !== 'function') {
232
+ throw new TypeError('touchSession must be a function');
233
+ }
231
234
  const owners = new Map();
232
235
 
233
236
  function hasActiveOwner(principalId, sessionId) {
@@ -238,6 +241,20 @@ function createAgentApiHttpAdapter(options = {}) {
238
241
  return false;
239
242
  }
240
243
 
244
+ function isSessionActive(input) {
245
+ return input !== null
246
+ && typeof input === 'object'
247
+ && hasActiveOwner(input.principalId, input.sessionId);
248
+ }
249
+
250
+ function touchSession(principalId, sessionId) {
251
+ try {
252
+ options.touchSession?.({ principalId, sessionId });
253
+ } catch {
254
+ // Activity bookkeeping must not replace the customer response.
255
+ }
256
+ }
257
+
241
258
  function requireOwner(principalId, sessionId, requestId) {
242
259
  const key = scopeKey(sessionId, requestId);
243
260
  const scopeOwners = owners.get(key);
@@ -313,9 +330,11 @@ function createAgentApiHttpAdapter(options = {}) {
313
330
  prompt,
314
331
  tools,
315
332
  }));
333
+ touchSession(principalId, sessionId);
316
334
  trackScope(key, principalId, internalSessionId, response);
317
335
  return jsonResponse(200, response);
318
336
  } catch (error) {
337
+ touchSession(principalId, sessionId);
319
338
  trackScope(key, principalId, internalSessionId, null);
320
339
  throw error;
321
340
  }
@@ -331,9 +350,11 @@ function createAgentApiHttpAdapter(options = {}) {
331
350
  requestId,
332
351
  toolResults: body.tool_results,
333
352
  }));
353
+ touchSession(principalId, sessionId);
334
354
  trackScope(owner.key, principalId, owner.internalSessionId, response);
335
355
  return jsonResponse(200, response);
336
356
  } catch (error) {
357
+ touchSession(principalId, sessionId);
337
358
  trackScope(owner.key, principalId, owner.internalSessionId, null);
338
359
  throw error;
339
360
  }
@@ -348,6 +369,7 @@ function createAgentApiHttpAdapter(options = {}) {
348
369
  requestId,
349
370
  reason: typeof body.reason === 'string' ? body.reason : undefined,
350
371
  });
372
+ touchSession(principalId, sessionId);
351
373
  trackScope(owner.key, principalId, owner.internalSessionId, null);
352
374
  return jsonResponse(200, {
353
375
  id: `agent-${requestId}`,
@@ -412,7 +434,7 @@ function createAgentApiHttpAdapter(options = {}) {
412
434
  }
413
435
  }
414
436
 
415
- return Object.freeze({ handleRequest });
437
+ return Object.freeze({ handleRequest, isSessionActive });
416
438
  }
417
439
 
418
440
  export {
@@ -0,0 +1,236 @@
1
+ import path from 'node:path';
2
+
3
+ import { createAgentApiServiceHost } from './agent-api-service-host.mjs';
4
+
5
+ const LOOPBACK_HOST = '127.0.0.1';
6
+ const PROVIDER_NAME = 'privateAgentProvider';
7
+ const PUBLIC_MODEL = 'king';
8
+
9
+ function requireEnvironment(env) {
10
+ if (env === null || typeof env !== 'object' || Array.isArray(env)) {
11
+ throw new TypeError('env must be an object');
12
+ }
13
+ return env;
14
+ }
15
+
16
+ function requiredValue(env, name, options = {}) {
17
+ const value = env[name];
18
+ if (typeof value !== 'string' || value.trim().length === 0) {
19
+ throw new TypeError(`${name} is required`);
20
+ }
21
+ const normalized = options.preserve === true ? value : value.trim();
22
+ return normalized;
23
+ }
24
+
25
+ function optionalValue(env, name) {
26
+ if (!Object.hasOwn(env, name)) return undefined;
27
+ const value = env[name];
28
+ if (typeof value !== 'string' || value.trim().length === 0) {
29
+ throw new TypeError(`${name} must be a non-empty string`);
30
+ }
31
+ return value.trim();
32
+ }
33
+
34
+ function integerValue(value, name, options = {}) {
35
+ if (!/^\d+$/u.test(value)) {
36
+ throw new TypeError(`${name} must be a positive safe integer`);
37
+ }
38
+ const parsed = Number(value);
39
+ const minimum = options.minimum ?? 1;
40
+ const maximum = options.maximum ?? Number.MAX_SAFE_INTEGER;
41
+ if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
42
+ if (options.rangeMessage !== undefined) {
43
+ throw new TypeError(`${name} ${options.rangeMessage}`);
44
+ }
45
+ if (minimum === 1024) {
46
+ throw new TypeError(`${name} must be an integer of at least 1024`);
47
+ }
48
+ throw new TypeError(`${name} must be a positive safe integer`);
49
+ }
50
+ return parsed;
51
+ }
52
+
53
+ function requireSecret(env, name) {
54
+ const value = requiredValue(env, name, { preserve: true });
55
+ if (Buffer.byteLength(value, 'utf8') < 32) {
56
+ throw new TypeError(`${name} must contain at least 32 bytes`);
57
+ }
58
+ return value;
59
+ }
60
+
61
+ function requireAbsoluteRoot(env) {
62
+ const value = requiredValue(env, 'BLUN_AGENT_API_ROOT_DIR');
63
+ if (!path.isAbsolute(value)) {
64
+ throw new TypeError('BLUN_AGENT_API_ROOT_DIR must be an absolute path');
65
+ }
66
+ return path.resolve(value);
67
+ }
68
+
69
+ function requireProviderBaseUrl(env) {
70
+ const value = requiredValue(env, 'BLUN_AGENT_API_PROVIDER_BASE_URL');
71
+ let parsed;
72
+ try {
73
+ parsed = new URL(value);
74
+ } catch {
75
+ throw new TypeError('BLUN_AGENT_API_PROVIDER_BASE_URL must be a valid URL');
76
+ }
77
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
78
+ throw new TypeError('BLUN_AGENT_API_PROVIDER_BASE_URL must use http or https');
79
+ }
80
+ if (parsed.username.length > 0 || parsed.password.length > 0) {
81
+ throw new TypeError('BLUN_AGENT_API_PROVIDER_BASE_URL must not contain credentials');
82
+ }
83
+ if (parsed.search.length > 0 || parsed.hash.length > 0) {
84
+ throw new TypeError('BLUN_AGENT_API_PROVIDER_BASE_URL must not contain a query or fragment');
85
+ }
86
+ return value;
87
+ }
88
+
89
+ function loadAgentApiServiceEnvironment(input = process.env) {
90
+ const env = requireEnvironment(input);
91
+ const rootDir = requireAbsoluteRoot(env);
92
+ const internalSecret = requireSecret(env, 'BLUN_AGENT_API_INTERNAL_SECRET');
93
+ const isolationSecret = requireSecret(env, 'BLUN_AGENT_API_ISOLATION_SECRET');
94
+ if (internalSecret === isolationSecret) {
95
+ throw new TypeError('Agent API internal and isolation secrets must be different');
96
+ }
97
+
98
+ const port = integerValue(
99
+ requiredValue(env, 'BLUN_AGENT_API_PORT'),
100
+ 'BLUN_AGENT_API_PORT',
101
+ { maximum: 65_535, rangeMessage: 'must be an integer from 1 through 65535' },
102
+ );
103
+ const providerBaseUrl = requireProviderBaseUrl(env);
104
+ const providerApiKey = requiredValue(
105
+ env,
106
+ 'BLUN_AGENT_API_PROVIDER_API_KEY',
107
+ { preserve: true },
108
+ );
109
+ const providerModel = requiredValue(env, 'BLUN_AGENT_API_PROVIDER_MODEL');
110
+ const maxContextSize = integerValue(
111
+ requiredValue(env, 'BLUN_AGENT_API_MAX_CONTEXT_TOKENS'),
112
+ 'BLUN_AGENT_API_MAX_CONTEXT_TOKENS',
113
+ );
114
+ const maxOutputSize = integerValue(
115
+ requiredValue(env, 'BLUN_AGENT_API_MAX_OUTPUT_TOKENS'),
116
+ 'BLUN_AGENT_API_MAX_OUTPUT_TOKENS',
117
+ );
118
+ if (maxOutputSize > maxContextSize) {
119
+ throw new TypeError(
120
+ 'BLUN_AGENT_API_MAX_OUTPUT_TOKENS must not exceed BLUN_AGENT_API_MAX_CONTEXT_TOKENS',
121
+ );
122
+ }
123
+ const maxBodyBytes = integerValue(
124
+ requiredValue(env, 'BLUN_AGENT_API_MAX_BODY_BYTES'),
125
+ 'BLUN_AGENT_API_MAX_BODY_BYTES',
126
+ { minimum: 1024 },
127
+ );
128
+ const maxPrincipals = integerValue(
129
+ requiredValue(env, 'BLUN_AGENT_API_MAX_PRINCIPALS'),
130
+ 'BLUN_AGENT_API_MAX_PRINCIPALS',
131
+ );
132
+ const maxSessionsPerPrincipal = integerValue(
133
+ requiredValue(env, 'BLUN_AGENT_API_MAX_SESSIONS_PER_PRINCIPAL'),
134
+ 'BLUN_AGENT_API_MAX_SESSIONS_PER_PRINCIPAL',
135
+ );
136
+
137
+ const idleTimeoutRaw = optionalValue(env, 'BLUN_AGENT_API_SESSION_IDLE_TIMEOUT_MS');
138
+ const idleSweepRaw = optionalValue(env, 'BLUN_AGENT_API_IDLE_SWEEP_INTERVAL_MS');
139
+ const sessionIdleTimeoutMs = idleTimeoutRaw === undefined
140
+ ? 0
141
+ : integerValue(idleTimeoutRaw, 'BLUN_AGENT_API_SESSION_IDLE_TIMEOUT_MS');
142
+ if (idleSweepRaw !== undefined && sessionIdleTimeoutMs === 0) {
143
+ throw new TypeError(
144
+ 'BLUN_AGENT_API_IDLE_SWEEP_INTERVAL_MS requires BLUN_AGENT_API_SESSION_IDLE_TIMEOUT_MS',
145
+ );
146
+ }
147
+ const idleSweepIntervalMs = idleSweepRaw === undefined
148
+ ? undefined
149
+ : integerValue(idleSweepRaw, 'BLUN_AGENT_API_IDLE_SWEEP_INTERVAL_MS');
150
+
151
+ const providerConfig = Object.freeze({
152
+ type: 'blun',
153
+ apiKey: providerApiKey,
154
+ baseUrl: providerBaseUrl,
155
+ defaultModel: providerModel,
156
+ });
157
+ const modelConfig = Object.freeze({
158
+ provider: PROVIDER_NAME,
159
+ model: providerModel,
160
+ maxContextSize,
161
+ maxOutputSize,
162
+ capabilities: Object.freeze(['tool_use']),
163
+ });
164
+ const harnessConfig = Object.freeze({
165
+ providers: Object.freeze({ [PROVIDER_NAME]: providerConfig }),
166
+ models: Object.freeze({ [PUBLIC_MODEL]: modelConfig }),
167
+ defaultModel: PUBLIC_MODEL,
168
+ });
169
+ const hostOptions = Object.freeze({
170
+ rootDir,
171
+ internalSecret,
172
+ isolationSecret,
173
+ harnessConfig,
174
+ maxBodyBytes,
175
+ maxPrincipals,
176
+ maxSessionsPerPrincipal,
177
+ sessionIdleTimeoutMs,
178
+ idleSweepIntervalMs,
179
+ });
180
+
181
+ return Object.freeze({
182
+ listen: Object.freeze({ host: LOOPBACK_HOST, port }),
183
+ hostOptions,
184
+ });
185
+ }
186
+
187
+ async function startAgentApiServiceFromEnvironment(options = {}) {
188
+ const createServiceHost = options.createServiceHost === undefined
189
+ ? createAgentApiServiceHost
190
+ : options.createServiceHost;
191
+ if (typeof createServiceHost !== 'function') {
192
+ throw new TypeError('createServiceHost must be a function');
193
+ }
194
+ const config = loadAgentApiServiceEnvironment(
195
+ options.env === undefined ? process.env : options.env,
196
+ );
197
+ const service = createServiceHost(config.hostOptions);
198
+ if (service === null || typeof service !== 'object'
199
+ || typeof service.listen !== 'function'
200
+ || typeof service.close !== 'function') {
201
+ throw new TypeError('Agent API service host must provide listen and close');
202
+ }
203
+
204
+ let address;
205
+ try {
206
+ address = await service.listen(config.listen);
207
+ } catch (error) {
208
+ try {
209
+ await service.close();
210
+ } catch {
211
+ // The listen failure is the actionable error; cleanup remains best-effort.
212
+ }
213
+ throw error;
214
+ }
215
+
216
+ let closePromise;
217
+ return Object.freeze({
218
+ address: Object.freeze({
219
+ address: address.address,
220
+ family: address.family,
221
+ port: address.port,
222
+ }),
223
+ close() {
224
+ closePromise ??= Promise.resolve().then(() => service.close());
225
+ return closePromise;
226
+ },
227
+ });
228
+ }
229
+
230
+ export {
231
+ LOOPBACK_HOST,
232
+ PROVIDER_NAME,
233
+ PUBLIC_MODEL,
234
+ loadAgentApiServiceEnvironment,
235
+ startAgentApiServiceFromEnvironment,
236
+ };
@@ -113,6 +113,7 @@ function createAgentApiServiceHost(options = {}) {
113
113
  runtime,
114
114
  publicModel: 'king',
115
115
  });
116
+ let adapter;
116
117
  const sessionRegistry = createAgentApiSessionRegistry({
117
118
  rootDir,
118
119
  isolationSecret,
@@ -121,11 +122,20 @@ function createAgentApiServiceHost(options = {}) {
121
122
  sessionOptions: options.sessionOptions,
122
123
  maxPrincipals: options.maxPrincipals,
123
124
  maxSessionsPerPrincipal: options.maxSessionsPerPrincipal,
125
+ sessionIdleTimeoutMs: options.sessionIdleTimeoutMs,
126
+ idleSweepIntervalMs: options.idleSweepIntervalMs,
127
+ now: options.now,
128
+ scheduleInterval: options.scheduleInterval,
129
+ cancelInterval: options.cancelInterval,
130
+ isSessionActive(input) {
131
+ return adapter?.isSessionActive(input) === true;
132
+ },
124
133
  });
125
- const adapter = createAgentApiHttpAdapter({
134
+ adapter = createAgentApiHttpAdapter({
126
135
  controller,
127
136
  resolveSession: sessionRegistry.resolveSession,
128
137
  closeSession: sessionRegistry.closeSession,
138
+ touchSession: sessionRegistry.touchSession,
129
139
  });
130
140
  const privateServer = createAgentApiPrivateHttpServer({
131
141
  adapter,
@@ -40,6 +40,13 @@ function requireLimit(value) {
40
40
  return value;
41
41
  }
42
42
 
43
+ function requireIdleDuration(value, label, { allowZero = true } = {}) {
44
+ if (!Number.isSafeInteger(value) || value < (allowZero ? 0 : 1)) {
45
+ throw new TypeError(`${label} must be ${allowZero ? 'a non-negative' : 'a positive'} integer`);
46
+ }
47
+ return value;
48
+ }
49
+
43
50
  function validateIsolationSecret(value) {
44
51
  if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') < 32) {
45
52
  throw new TypeError('isolationSecret must contain at least 32 bytes');
@@ -98,8 +105,37 @@ function createAgentApiSessionRegistry(options = {}) {
98
105
  options.maxSessionsPerPrincipal ?? DEFAULT_MAX_SESSIONS_PER_PRINCIPAL,
99
106
  );
100
107
  const maxPrincipals = requireLimit(options.maxPrincipals ?? DEFAULT_MAX_PRINCIPALS);
108
+ const sessionIdleTimeoutMs = requireIdleDuration(
109
+ options.sessionIdleTimeoutMs ?? 0,
110
+ 'sessionIdleTimeoutMs',
111
+ );
112
+ if (sessionIdleTimeoutMs === 0 && options.idleSweepIntervalMs !== undefined) {
113
+ throw new TypeError('idleSweepIntervalMs requires sessionIdleTimeoutMs');
114
+ }
115
+ const idleSweepIntervalMs = sessionIdleTimeoutMs === 0
116
+ ? 0
117
+ : requireIdleDuration(
118
+ options.idleSweepIntervalMs ?? Math.min(sessionIdleTimeoutMs, 60_000),
119
+ 'idleSweepIntervalMs',
120
+ { allowZero: false },
121
+ );
122
+ const now = options.now ?? Date.now;
123
+ const scheduleInterval = options.scheduleInterval ?? setInterval;
124
+ const cancelInterval = options.cancelInterval ?? clearInterval;
125
+ if (typeof now !== 'function') throw new TypeError('now must be a function');
126
+ if (typeof scheduleInterval !== 'function') {
127
+ throw new TypeError('scheduleInterval must be a function');
128
+ }
129
+ if (typeof cancelInterval !== 'function') {
130
+ throw new TypeError('cancelInterval must be a function');
131
+ }
132
+ if (options.isSessionActive !== undefined && typeof options.isSessionActive !== 'function') {
133
+ throw new TypeError('isSessionActive must be a function');
134
+ }
101
135
  const principals = new Map();
102
136
  let closed = false;
137
+ let sweepHandle;
138
+ let sweepPromise = Promise.resolve(0);
103
139
 
104
140
  function ensureOpen() {
105
141
  if (closed) {
@@ -210,11 +246,22 @@ function createAgentApiSessionRegistry(options = {}) {
210
246
  );
211
247
  let record = principals.get(principalId);
212
248
  const existing = record?.sessions.get(publicSessionId);
213
- if (existing !== undefined) return existing;
249
+ if (existing !== undefined) {
250
+ if (existing.closingPromise !== undefined) {
251
+ await existing.closingPromise;
252
+ return resolveSession(input);
253
+ }
254
+ existing.lastActiveAt = now();
255
+ return existing.pending;
256
+ }
214
257
  if (input.create !== true) {
215
258
  throw registryError('AGENT_API_SESSION_NOT_FOUND', 'Agent API session was not found');
216
259
  }
217
260
  record = getOrCreatePrincipal(principalId);
261
+ if (record.sessions.size >= maxSessionsPerPrincipal) {
262
+ await pruneIdleSessions({ principalId });
263
+ record = principals.get(principalId) ?? getOrCreatePrincipal(principalId);
264
+ }
218
265
  if (record.sessions.size >= maxSessionsPerPrincipal) {
219
266
  throw registryError(
220
267
  'AGENT_API_SESSION_LIMIT',
@@ -222,12 +269,22 @@ function createAgentApiSessionRegistry(options = {}) {
222
269
  );
223
270
  }
224
271
 
225
- const pending = createSession(record, principalId, publicSessionId);
226
- record.sessions.set(publicSessionId, pending);
272
+ const entry = {
273
+ closingPromise: undefined,
274
+ lastActiveAt: now(),
275
+ pending: undefined,
276
+ ready: false,
277
+ };
278
+ entry.pending = createSession(record, principalId, publicSessionId).then((session) => {
279
+ entry.ready = true;
280
+ entry.lastActiveAt = now();
281
+ return session;
282
+ });
283
+ record.sessions.set(publicSessionId, entry);
227
284
  try {
228
- return await pending;
285
+ return await entry.pending;
229
286
  } catch (error) {
230
- if (record.sessions.get(publicSessionId) === pending) {
287
+ if (record.sessions.get(publicSessionId) === entry) {
231
288
  record.sessions.delete(publicSessionId);
232
289
  }
233
290
  if (!record.harnessReady
@@ -239,6 +296,74 @@ function createAgentApiSessionRegistry(options = {}) {
239
296
  }
240
297
  }
241
298
 
299
+ function touchSession(input) {
300
+ if (closed || input === null || typeof input !== 'object') return false;
301
+ const record = principals.get(input.principalId);
302
+ const entry = record?.sessions.get(input.sessionId);
303
+ if (entry === undefined || entry.closingPromise !== undefined) return false;
304
+ entry.lastActiveAt = now();
305
+ return true;
306
+ }
307
+
308
+ function sessionIsActive(principalId, publicSessionId) {
309
+ if (options.isSessionActive === undefined) return false;
310
+ try {
311
+ const result = options.isSessionActive({ principalId, sessionId: publicSessionId });
312
+ return result === true || result?.then !== undefined;
313
+ } catch {
314
+ return true;
315
+ }
316
+ }
317
+
318
+ async function closeEntry(record, principalId, publicSessionId, entry) {
319
+ if (entry.closingPromise !== undefined) return entry.closingPromise;
320
+ entry.closingPromise = (async () => {
321
+ let failure;
322
+ try {
323
+ const session = await entry.pending;
324
+ await session.close?.();
325
+ } catch (error) {
326
+ failure = error;
327
+ }
328
+ if (record.sessions.get(publicSessionId) === entry) {
329
+ record.sessions.delete(publicSessionId);
330
+ }
331
+ if (record.sessions.size === 0 && principals.get(principalId) === record) {
332
+ principals.delete(principalId);
333
+ const harness = await record.harnessPromise.catch(() => null);
334
+ try {
335
+ await harness?.close?.();
336
+ } catch (error) {
337
+ failure ??= error;
338
+ }
339
+ }
340
+ if (failure !== undefined) throw failure;
341
+ return true;
342
+ })();
343
+ return entry.closingPromise;
344
+ }
345
+
346
+ async function pruneIdleSessions(input = {}) {
347
+ if (closed || sessionIdleTimeoutMs === 0) return 0;
348
+ const principalFilter = input.principalId;
349
+ const cutoff = now() - sessionIdleTimeoutMs;
350
+ const closing = [];
351
+ for (const [principalId, record] of principals) {
352
+ if (principalFilter !== undefined && principalId !== principalFilter) continue;
353
+ for (const [publicSessionId, entry] of record.sessions) {
354
+ if (!entry.ready
355
+ || entry.closingPromise !== undefined
356
+ || entry.lastActiveAt > cutoff
357
+ || sessionIsActive(principalId, publicSessionId)) {
358
+ continue;
359
+ }
360
+ closing.push(closeEntry(record, principalId, publicSessionId, entry));
361
+ }
362
+ }
363
+ const outcomes = await Promise.allSettled(closing);
364
+ return outcomes.filter((outcome) => outcome.status === 'fulfilled').length;
365
+ }
366
+
242
367
  async function closeSession(input) {
243
368
  ensureOpen();
244
369
  if (input === null || typeof input !== 'object') {
@@ -251,36 +376,48 @@ function createAgentApiSessionRegistry(options = {}) {
251
376
  'sessionId',
252
377
  );
253
378
  const record = principals.get(principalId);
254
- const pending = record?.sessions.get(publicSessionId);
255
- if (pending === undefined) return false;
256
- record.sessions.delete(publicSessionId);
257
- const session = await pending;
258
- await session.close?.();
259
- if (record.sessions.size === 0 && principals.get(principalId) === record) {
260
- principals.delete(principalId);
261
- const harness = await record.harnessPromise.catch(() => null);
262
- await harness?.close?.();
263
- }
264
- return true;
379
+ const entry = record?.sessions.get(publicSessionId);
380
+ if (entry === undefined) return false;
381
+ return closeEntry(record, principalId, publicSessionId, entry);
265
382
  }
266
383
 
267
384
  async function close() {
268
385
  if (closed) return;
269
386
  closed = true;
387
+ if (sweepHandle !== undefined) cancelInterval(sweepHandle);
388
+ await sweepPromise;
270
389
  const records = [...principals.values()];
271
390
  principals.clear();
272
391
  for (const record of records) {
273
- const sessions = await Promise.allSettled([...record.sessions.values()]);
392
+ const sessions = await Promise.allSettled(
393
+ [...record.sessions.values()].map(async (entry) => {
394
+ if (entry.closingPromise !== undefined) {
395
+ await entry.closingPromise;
396
+ return null;
397
+ }
398
+ return entry.pending;
399
+ }),
400
+ );
274
401
  record.sessions.clear();
275
402
  for (const outcome of sessions) {
276
- if (outcome.status === 'fulfilled') await outcome.value.close?.();
403
+ if (outcome.status === 'fulfilled' && outcome.value !== null) {
404
+ await outcome.value.close?.();
405
+ }
277
406
  }
278
407
  const harness = await record.harnessPromise.catch(() => null);
279
408
  await harness?.close?.();
280
409
  }
281
410
  }
282
411
 
283
- return Object.freeze({ close, closeSession, resolveSession });
412
+ if (sessionIdleTimeoutMs > 0) {
413
+ sweepHandle = scheduleInterval(() => {
414
+ sweepPromise = sweepPromise.then(() => pruneIdleSessions()).catch(() => 0);
415
+ return sweepPromise;
416
+ }, idleSweepIntervalMs);
417
+ sweepHandle?.unref?.();
418
+ }
419
+
420
+ return Object.freeze({ close, closeSession, pruneIdleSessions, resolveSession, touchSession });
284
421
  }
285
422
 
286
423
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.474",
3
+ "version": "9.1.476",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {