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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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 {
|