blun-king-cli 9.1.473 → 9.1.475

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
  }
@@ -325,13 +344,20 @@ function createAgentApiHttpAdapter(options = {}) {
325
344
  const body = requireBody(input.body);
326
345
  const sessionId = requirePublicId(body.session_id, 'session_id');
327
346
  const owner = requireOwner(principalId, sessionId, requestId);
328
- const response = publicCompletion(await controller.continueTurn({
329
- sessionId: owner.internalSessionId,
330
- requestId,
331
- toolResults: body.tool_results,
332
- }));
333
- trackScope(owner.key, principalId, owner.internalSessionId, response);
334
- return jsonResponse(200, response);
347
+ try {
348
+ const response = publicCompletion(await controller.continueTurn({
349
+ sessionId: owner.internalSessionId,
350
+ requestId,
351
+ toolResults: body.tool_results,
352
+ }));
353
+ touchSession(principalId, sessionId);
354
+ trackScope(owner.key, principalId, owner.internalSessionId, response);
355
+ return jsonResponse(200, response);
356
+ } catch (error) {
357
+ touchSession(principalId, sessionId);
358
+ trackScope(owner.key, principalId, owner.internalSessionId, null);
359
+ throw error;
360
+ }
335
361
  }
336
362
 
337
363
  async function handleCancel(input, principalId, requestId) {
@@ -343,6 +369,7 @@ function createAgentApiHttpAdapter(options = {}) {
343
369
  requestId,
344
370
  reason: typeof body.reason === 'string' ? body.reason : undefined,
345
371
  });
372
+ touchSession(principalId, sessionId);
346
373
  trackScope(owner.key, principalId, owner.internalSessionId, null);
347
374
  return jsonResponse(200, {
348
375
  id: `agent-${requestId}`,
@@ -407,7 +434,7 @@ function createAgentApiHttpAdapter(options = {}) {
407
434
  }
408
435
  }
409
436
 
410
- return Object.freeze({ handleRequest });
437
+ return Object.freeze({ handleRequest, isSessionActive });
411
438
  }
412
439
 
413
440
  export {
@@ -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.473",
3
+ "version": "9.1.475",
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": {