@poolzin/pool-bot 2026.2.20 → 2026.2.21
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.
- package/CHANGELOG.md +8 -0
- package/dist/agents/model-auth.js +12 -0
- package/dist/agents/model-fallback.js +24 -0
- package/dist/agents/pi-embedded-runner/run/attempt.js +15 -0
- package/dist/agents/provider/config-loader.js +76 -0
- package/dist/agents/provider/index.js +15 -0
- package/dist/agents/provider/integration.js +136 -0
- package/dist/agents/provider/models-dev.js +129 -0
- package/dist/agents/provider/rate-limits.js +458 -0
- package/dist/agents/provider/request-monitor.js +449 -0
- package/dist/agents/provider/session-binding.js +376 -0
- package/dist/agents/provider/token-pool.js +541 -0
- package/dist/build-info.json +3 -3
- package/package.json +1 -1
- package/skills/plcode-controller/SKILL.md +156 -0
- package/skills/plcode-controller/assets/operator-prompts.md +65 -0
- package/skills/plcode-controller/references/command-cheatsheet.md +53 -0
- package/skills/plcode-controller/references/failure-handling.md +60 -0
- package/skills/plcode-controller/references/model-selection.md +57 -0
- package/skills/plcode-controller/references/plan-vs-build.md +52 -0
- package/skills/plcode-controller/references/question-handling.md +40 -0
- package/skills/plcode-controller/references/session-management.md +63 -0
- package/skills/plcode-controller/references/workflow.md +35 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages sticky session bindings between conversation sessions and API tokens.
|
|
5
|
+
* Ensures conversation continuity by routing requests to the same key when possible.
|
|
6
|
+
*
|
|
7
|
+
* @module provider/session-binding
|
|
8
|
+
*/
|
|
9
|
+
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
|
10
|
+
export var SessionManager;
|
|
11
|
+
(function (SessionManager) {
|
|
12
|
+
const log = createSubsystemLogger("provider/session-binding");
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Internal State
|
|
15
|
+
// ============================================================================
|
|
16
|
+
/** Session bindings: sessionID:providerID -> Binding */
|
|
17
|
+
const bindings = new Map();
|
|
18
|
+
/** Configuration */
|
|
19
|
+
let config = {
|
|
20
|
+
defaultTTLMs: 60_000, // 60 seconds
|
|
21
|
+
maxBindings: 10_000,
|
|
22
|
+
extendOnAccess: true,
|
|
23
|
+
};
|
|
24
|
+
/** Cleanup interval handle */
|
|
25
|
+
let cleanupInterval = null;
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Helper Functions
|
|
28
|
+
// ============================================================================
|
|
29
|
+
/**
|
|
30
|
+
* Creates a composite key for the bindings map.
|
|
31
|
+
*/
|
|
32
|
+
function getBindingKey(sessionID, providerID) {
|
|
33
|
+
return `${sessionID}:${providerID}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Checks if a binding is expired.
|
|
37
|
+
*/
|
|
38
|
+
function isExpired(binding) {
|
|
39
|
+
return Date.now() > binding.expiresAt;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Evicts oldest bindings if over capacity.
|
|
43
|
+
*/
|
|
44
|
+
function evictIfNeeded() {
|
|
45
|
+
if (bindings.size <= config.maxBindings)
|
|
46
|
+
return;
|
|
47
|
+
// Sort by last accessed time and remove oldest
|
|
48
|
+
const sorted = Array.from(bindings.entries()).sort((a, b) => a[1].lastAccessedAt - b[1].lastAccessedAt);
|
|
49
|
+
const toRemove = sorted.slice(0, bindings.size - config.maxBindings);
|
|
50
|
+
for (const [key] of toRemove) {
|
|
51
|
+
bindings.delete(key);
|
|
52
|
+
}
|
|
53
|
+
log.info("evicted", { count: toRemove.length });
|
|
54
|
+
}
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Public API
|
|
57
|
+
// ============================================================================
|
|
58
|
+
/**
|
|
59
|
+
* Configures the session manager.
|
|
60
|
+
*
|
|
61
|
+
* @param newConfig - Partial configuration to merge
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* SessionManager.configure({
|
|
66
|
+
* defaultTTLMs: 120_000, // 2 minutes
|
|
67
|
+
* extendOnAccess: true
|
|
68
|
+
* })
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
function configure(newConfig) {
|
|
72
|
+
config = { ...config, ...newConfig };
|
|
73
|
+
log.info("configured", { ...config });
|
|
74
|
+
}
|
|
75
|
+
SessionManager.configure = configure;
|
|
76
|
+
/**
|
|
77
|
+
* Gets the current configuration.
|
|
78
|
+
*/
|
|
79
|
+
function getConfig() {
|
|
80
|
+
return { ...config };
|
|
81
|
+
}
|
|
82
|
+
SessionManager.getConfig = getConfig;
|
|
83
|
+
/**
|
|
84
|
+
* Creates or updates a session binding.
|
|
85
|
+
*
|
|
86
|
+
* @param sessionID - Session identifier
|
|
87
|
+
* @param providerID - Provider identifier
|
|
88
|
+
* @param tokenID - Token identifier to bind to
|
|
89
|
+
* @param ttlMs - Custom TTL (uses default if not specified)
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* SessionManager.bind("session-123", "anthropic", "key-1")
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
function bind(sessionID, providerID, tokenID, ttlMs) {
|
|
97
|
+
const key = getBindingKey(sessionID, providerID);
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const ttl = ttlMs ?? config.defaultTTLMs;
|
|
100
|
+
const existing = bindings.get(key);
|
|
101
|
+
if (existing && existing.tokenID === tokenID) {
|
|
102
|
+
// Update existing binding
|
|
103
|
+
existing.lastAccessedAt = now;
|
|
104
|
+
existing.expiresAt = now + ttl;
|
|
105
|
+
existing.requestCount++;
|
|
106
|
+
return existing;
|
|
107
|
+
}
|
|
108
|
+
// Create new binding
|
|
109
|
+
const binding = {
|
|
110
|
+
sessionID,
|
|
111
|
+
providerID,
|
|
112
|
+
tokenID,
|
|
113
|
+
createdAt: now,
|
|
114
|
+
lastAccessedAt: now,
|
|
115
|
+
expiresAt: now + ttl,
|
|
116
|
+
requestCount: 1,
|
|
117
|
+
};
|
|
118
|
+
bindings.set(key, binding);
|
|
119
|
+
evictIfNeeded();
|
|
120
|
+
log.info("bound", { sessionID, providerID, tokenID, ttlMs: ttl });
|
|
121
|
+
return binding;
|
|
122
|
+
}
|
|
123
|
+
SessionManager.bind = bind;
|
|
124
|
+
/**
|
|
125
|
+
* Looks up a session binding.
|
|
126
|
+
*
|
|
127
|
+
* @param sessionID - Session identifier
|
|
128
|
+
* @param providerID - Provider identifier
|
|
129
|
+
* @returns Lookup result with binding if found and valid
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* const result = SessionManager.lookup("session-123", "anthropic")
|
|
134
|
+
* if (result.found && !result.expired) {
|
|
135
|
+
* console.log("Using token:", result.binding.tokenID)
|
|
136
|
+
* }
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
function lookup(sessionID, providerID) {
|
|
140
|
+
const key = getBindingKey(sessionID, providerID);
|
|
141
|
+
const binding = bindings.get(key);
|
|
142
|
+
if (!binding) {
|
|
143
|
+
return { found: false, expired: false };
|
|
144
|
+
}
|
|
145
|
+
if (isExpired(binding)) {
|
|
146
|
+
bindings.delete(key);
|
|
147
|
+
return { found: true, expired: true };
|
|
148
|
+
}
|
|
149
|
+
// Extend TTL on access if configured
|
|
150
|
+
if (config.extendOnAccess) {
|
|
151
|
+
binding.lastAccessedAt = Date.now();
|
|
152
|
+
binding.expiresAt = binding.lastAccessedAt + config.defaultTTLMs;
|
|
153
|
+
}
|
|
154
|
+
return { binding, found: true, expired: false };
|
|
155
|
+
}
|
|
156
|
+
SessionManager.lookup = lookup;
|
|
157
|
+
/**
|
|
158
|
+
* Gets the bound token ID for a session, if any.
|
|
159
|
+
*
|
|
160
|
+
* @param sessionID - Session identifier
|
|
161
|
+
* @param providerID - Provider identifier
|
|
162
|
+
* @returns Token ID or undefined if not bound
|
|
163
|
+
*/
|
|
164
|
+
function getBoundToken(sessionID, providerID) {
|
|
165
|
+
const result = lookup(sessionID, providerID);
|
|
166
|
+
return result.binding?.tokenID;
|
|
167
|
+
}
|
|
168
|
+
SessionManager.getBoundToken = getBoundToken;
|
|
169
|
+
/**
|
|
170
|
+
* Removes a session binding.
|
|
171
|
+
*
|
|
172
|
+
* @param sessionID - Session identifier
|
|
173
|
+
* @param providerID - Provider identifier
|
|
174
|
+
* @returns Whether a binding was found and removed
|
|
175
|
+
*/
|
|
176
|
+
function unbind(sessionID, providerID) {
|
|
177
|
+
const key = getBindingKey(sessionID, providerID);
|
|
178
|
+
const removed = bindings.delete(key);
|
|
179
|
+
if (removed) {
|
|
180
|
+
log.info("unbound", { sessionID, providerID });
|
|
181
|
+
}
|
|
182
|
+
return removed;
|
|
183
|
+
}
|
|
184
|
+
SessionManager.unbind = unbind;
|
|
185
|
+
/**
|
|
186
|
+
* Removes all bindings for a session.
|
|
187
|
+
*
|
|
188
|
+
* @param sessionID - Session identifier
|
|
189
|
+
* @returns Number of bindings removed
|
|
190
|
+
*/
|
|
191
|
+
function unbindSession(sessionID) {
|
|
192
|
+
let count = 0;
|
|
193
|
+
const prefix = `${sessionID}:`;
|
|
194
|
+
for (const key of bindings.keys()) {
|
|
195
|
+
if (key.startsWith(prefix)) {
|
|
196
|
+
bindings.delete(key);
|
|
197
|
+
count++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (count > 0) {
|
|
201
|
+
log.info("session-unbound", { sessionID, count });
|
|
202
|
+
}
|
|
203
|
+
return count;
|
|
204
|
+
}
|
|
205
|
+
SessionManager.unbindSession = unbindSession;
|
|
206
|
+
/**
|
|
207
|
+
* Removes all bindings for a provider.
|
|
208
|
+
*
|
|
209
|
+
* @param providerID - Provider identifier
|
|
210
|
+
* @returns Number of bindings removed
|
|
211
|
+
*/
|
|
212
|
+
function unbindProvider(providerID) {
|
|
213
|
+
let count = 0;
|
|
214
|
+
const suffix = `:${providerID}`;
|
|
215
|
+
for (const key of bindings.keys()) {
|
|
216
|
+
if (key.endsWith(suffix)) {
|
|
217
|
+
bindings.delete(key);
|
|
218
|
+
count++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (count > 0) {
|
|
222
|
+
log.info("provider-unbound", { providerID, count });
|
|
223
|
+
}
|
|
224
|
+
return count;
|
|
225
|
+
}
|
|
226
|
+
SessionManager.unbindProvider = unbindProvider;
|
|
227
|
+
/**
|
|
228
|
+
* Removes all bindings for a specific token.
|
|
229
|
+
*
|
|
230
|
+
* @param providerID - Provider identifier
|
|
231
|
+
* @param tokenID - Token identifier
|
|
232
|
+
* @returns Number of bindings removed
|
|
233
|
+
*/
|
|
234
|
+
function unbindToken(providerID, tokenID) {
|
|
235
|
+
let count = 0;
|
|
236
|
+
for (const [key, binding] of bindings.entries()) {
|
|
237
|
+
if (binding.providerID === providerID && binding.tokenID === tokenID) {
|
|
238
|
+
bindings.delete(key);
|
|
239
|
+
count++;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (count > 0) {
|
|
243
|
+
log.info("token-unbound", { providerID, tokenID, count });
|
|
244
|
+
}
|
|
245
|
+
return count;
|
|
246
|
+
}
|
|
247
|
+
SessionManager.unbindToken = unbindToken;
|
|
248
|
+
/**
|
|
249
|
+
* Gets all bindings for a session.
|
|
250
|
+
*
|
|
251
|
+
* @param sessionID - Session identifier
|
|
252
|
+
* @returns Array of bindings
|
|
253
|
+
*/
|
|
254
|
+
function getSessionBindings(sessionID) {
|
|
255
|
+
const result = [];
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
const prefix = `${sessionID}:`;
|
|
258
|
+
for (const [key, binding] of bindings.entries()) {
|
|
259
|
+
if (key.startsWith(prefix) && binding.expiresAt > now) {
|
|
260
|
+
result.push({ ...binding });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
SessionManager.getSessionBindings = getSessionBindings;
|
|
266
|
+
/**
|
|
267
|
+
* Gets all bindings for a provider.
|
|
268
|
+
*
|
|
269
|
+
* @param providerID - Provider identifier
|
|
270
|
+
* @returns Array of bindings
|
|
271
|
+
*/
|
|
272
|
+
function getProviderBindings(providerID) {
|
|
273
|
+
const result = [];
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
for (const binding of bindings.values()) {
|
|
276
|
+
if (binding.providerID === providerID && binding.expiresAt > now) {
|
|
277
|
+
result.push({ ...binding });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
SessionManager.getProviderBindings = getProviderBindings;
|
|
283
|
+
/**
|
|
284
|
+
* Gets summary statistics for session bindings.
|
|
285
|
+
*/
|
|
286
|
+
function getSummary() {
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
let activeCount = 0;
|
|
289
|
+
let expiredCount = 0;
|
|
290
|
+
let totalRequests = 0;
|
|
291
|
+
let totalAge = 0;
|
|
292
|
+
const byProvider = {};
|
|
293
|
+
for (const binding of bindings.values()) {
|
|
294
|
+
if (binding.expiresAt > now) {
|
|
295
|
+
activeCount++;
|
|
296
|
+
totalRequests += binding.requestCount;
|
|
297
|
+
totalAge += now - binding.createdAt;
|
|
298
|
+
byProvider[binding.providerID] = (byProvider[binding.providerID] ?? 0) + 1;
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
expiredCount++;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
totalBindings: bindings.size,
|
|
306
|
+
activeBindings: activeCount,
|
|
307
|
+
expiredBindings: expiredCount,
|
|
308
|
+
byProvider,
|
|
309
|
+
avgRequestsPerBinding: activeCount > 0 ? totalRequests / activeCount : 0,
|
|
310
|
+
avgAgeMs: activeCount > 0 ? totalAge / activeCount : 0,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
SessionManager.getSummary = getSummary;
|
|
314
|
+
/**
|
|
315
|
+
* Removes expired bindings.
|
|
316
|
+
*
|
|
317
|
+
* @returns Number of bindings removed
|
|
318
|
+
*/
|
|
319
|
+
function cleanupExpired() {
|
|
320
|
+
const now = Date.now();
|
|
321
|
+
let removed = 0;
|
|
322
|
+
for (const [key, binding] of bindings.entries()) {
|
|
323
|
+
if (binding.expiresAt <= now) {
|
|
324
|
+
bindings.delete(key);
|
|
325
|
+
removed++;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (removed > 0) {
|
|
329
|
+
log.info("cleanup", { removed });
|
|
330
|
+
}
|
|
331
|
+
return removed;
|
|
332
|
+
}
|
|
333
|
+
SessionManager.cleanupExpired = cleanupExpired;
|
|
334
|
+
/**
|
|
335
|
+
* Clears all bindings.
|
|
336
|
+
*/
|
|
337
|
+
function clear() {
|
|
338
|
+
const count = bindings.size;
|
|
339
|
+
bindings.clear();
|
|
340
|
+
if (count > 0) {
|
|
341
|
+
log.info("cleared", { count });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
SessionManager.clear = clear;
|
|
345
|
+
/**
|
|
346
|
+
* Starts the automatic cleanup interval.
|
|
347
|
+
*
|
|
348
|
+
* @param intervalMs - Cleanup interval in milliseconds (default: 30000)
|
|
349
|
+
*/
|
|
350
|
+
function startCleanup(intervalMs = 30_000) {
|
|
351
|
+
if (cleanupInterval)
|
|
352
|
+
return;
|
|
353
|
+
cleanupInterval = setInterval(() => {
|
|
354
|
+
cleanupExpired();
|
|
355
|
+
}, intervalMs);
|
|
356
|
+
// Don't keep the process alive just for cleanup
|
|
357
|
+
if (typeof cleanupInterval.unref === "function") {
|
|
358
|
+
cleanupInterval.unref();
|
|
359
|
+
}
|
|
360
|
+
log.debug("cleanup-started", { intervalMs });
|
|
361
|
+
}
|
|
362
|
+
SessionManager.startCleanup = startCleanup;
|
|
363
|
+
/**
|
|
364
|
+
* Stops the automatic cleanup interval.
|
|
365
|
+
*/
|
|
366
|
+
function stopCleanup() {
|
|
367
|
+
if (cleanupInterval) {
|
|
368
|
+
clearInterval(cleanupInterval);
|
|
369
|
+
cleanupInterval = null;
|
|
370
|
+
log.info("cleanup-stopped");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
SessionManager.stopCleanup = stopCleanup;
|
|
374
|
+
// Start cleanup on module load
|
|
375
|
+
startCleanup();
|
|
376
|
+
})(SessionManager || (SessionManager = {}));
|