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
|
-
|
|
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 {
|