blun-king-cli 9.1.474 → 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
|
}
|
|
@@ -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 {
|
|
@@ -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
|
-
|
|
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)
|
|
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
|
|
226
|
-
|
|
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) ===
|
|
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
|
|
255
|
-
if (
|
|
256
|
-
record
|
|
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(
|
|
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'
|
|
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
|
-
|
|
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 {
|