@bermudi/pi-delegate 0.1.0
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/README.md +92 -0
- package/agents.ts +347 -0
- package/concurrency.ts +126 -0
- package/config.ts +358 -0
- package/constants.ts +41 -0
- package/delegate.ts +115 -0
- package/dispatch.ts +362 -0
- package/extension.ts +126 -0
- package/file-tracking.ts +57 -0
- package/format.ts +506 -0
- package/host-compat.ts +73 -0
- package/host.ts +814 -0
- package/lifecycle.ts +704 -0
- package/manual.ts +184 -0
- package/model.ts +81 -0
- package/package.json +43 -0
- package/parent-context.ts +42 -0
- package/pool.ts +420 -0
- package/render-branches.ts +380 -0
- package/render-result.ts +182 -0
- package/runner.ts +686 -0
- package/schema.ts +289 -0
- package/sessions.ts +102 -0
- package/settings.ts +78 -0
- package/spill.ts +161 -0
- package/task-resolution.ts +321 -0
- package/tickets.ts +795 -0
- package/timer.ts +46 -0
- package/tools.ts +41 -0
- package/types.ts +243 -0
- package/usage.ts +121 -0
- package/utils.ts +131 -0
package/pool.ts
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type {
|
|
4
|
+
AgentSession,
|
|
5
|
+
SessionManager,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { fmtDuration, fmtTokens, shortenPath } from "./format.ts";
|
|
8
|
+
|
|
9
|
+
// ── Public value types (cross the seam) ───────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/** Immutable config captured when a session enters the pool. Write-once: once
|
|
12
|
+
* stored it never changes — only stats mutate, and only via {@link commit}.
|
|
13
|
+
* Reuse always validates cwd/thinking/tools and validates an explicitly
|
|
14
|
+
* requested model or base prompt against this frozen configuration. */
|
|
15
|
+
export interface FrozenConfig {
|
|
16
|
+
systemPrompt: string;
|
|
17
|
+
model: Model<Api>;
|
|
18
|
+
thinking: ThinkingLevel;
|
|
19
|
+
tools: string[];
|
|
20
|
+
cwd: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The subset a reuse request supplies for validation. `model` and
|
|
24
|
+
* `systemPrompt` are present only when this call explicitly requested them;
|
|
25
|
+
* omission means continue with the frozen live session configuration. */
|
|
26
|
+
export interface ConfigCandidate {
|
|
27
|
+
cwd: string;
|
|
28
|
+
thinking: ThinkingLevel;
|
|
29
|
+
tools: string[];
|
|
30
|
+
model?: Model<Api>;
|
|
31
|
+
systemPrompt?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One field-level diff from a reuse that conflicts with the frozen config. The
|
|
35
|
+
* pool computes these; the caller formats the error string. */
|
|
36
|
+
export interface ConfigMismatch {
|
|
37
|
+
field: "cwd" | "thinking" | "tools" | "model" | "systemPrompt";
|
|
38
|
+
frozen: string;
|
|
39
|
+
requested: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Result of {@link checkout}. Discriminated so the caller handles
|
|
43
|
+
* hit/miss/mismatch without parsing. checkout is PURE — it does not bump
|
|
44
|
+
* lastUsed (that is commit's job), so a speculative checkout that bails leaves
|
|
45
|
+
* no trace. */
|
|
46
|
+
export type CheckoutResult =
|
|
47
|
+
| {
|
|
48
|
+
status: "hit";
|
|
49
|
+
session: AgentSession;
|
|
50
|
+
sessionManager: SessionManager;
|
|
51
|
+
sessionFile: string;
|
|
52
|
+
/** Frozen model id — caller uses it for the progress row / display. */
|
|
53
|
+
modelId: string;
|
|
54
|
+
}
|
|
55
|
+
| { status: "miss" }
|
|
56
|
+
| { status: "mismatch"; mismatches: ConfigMismatch[] };
|
|
57
|
+
|
|
58
|
+
/** Payload for {@link commit}. The caller always assembles the full payload on
|
|
59
|
+
* a successful run; commit decides insert-vs-recordUse by map presence (sound
|
|
60
|
+
* because the per-session lock serializes same-sessionId tasks). */
|
|
61
|
+
export interface CommitPayload {
|
|
62
|
+
session: AgentSession;
|
|
63
|
+
sessionManager: SessionManager | undefined;
|
|
64
|
+
sessionFile: string | undefined;
|
|
65
|
+
frozen: FrozenConfig;
|
|
66
|
+
tokens: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Internal state (PRIVATE — callers cross the seam, never the Map) ───────
|
|
70
|
+
|
|
71
|
+
interface PooledAgent {
|
|
72
|
+
/** The live AgentSession — reused across prompts for this sessionId. */
|
|
73
|
+
session: AgentSession;
|
|
74
|
+
sessionManager: SessionManager;
|
|
75
|
+
sessionFile: string;
|
|
76
|
+
/** Config frozen at creation time — validated against on reuse. */
|
|
77
|
+
config: FrozenConfig;
|
|
78
|
+
lastUsed: number;
|
|
79
|
+
createdAt: number;
|
|
80
|
+
/** Total tokens consumed across all prompts on this session. */
|
|
81
|
+
totalTokens: number;
|
|
82
|
+
/** Number of prompts sent to this session. */
|
|
83
|
+
promptCount: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Module-level pool — lives for the entire Pi session. Not exported: callers
|
|
87
|
+
* use checkout/commit/configFor/closePooledAgent/closeAllPooledAgents/listPooledAgents. */
|
|
88
|
+
const agentPool = new Map<string, PooledAgent>();
|
|
89
|
+
|
|
90
|
+
// AgentSession.abort() normally settles promptly, but a provider or tool can
|
|
91
|
+
// ignore cancellation. Cleanup must not make parent-session shutdown hostage to
|
|
92
|
+
// that promise, so close operations use a bounded wait before forced disposal.
|
|
93
|
+
const DEFAULT_POOL_ABORT_TIMEOUT_MS = 10_000;
|
|
94
|
+
let poolAbortTimeoutMs = DEFAULT_POOL_ABORT_TIMEOUT_MS;
|
|
95
|
+
|
|
96
|
+
/** Per-session lock — serializes access to a pooled agent so concurrent
|
|
97
|
+
* delegate calls with the same sessionId queue instead of interleaving. */
|
|
98
|
+
const sessionLocks = new Map<string, Promise<void>>();
|
|
99
|
+
|
|
100
|
+
function now(): number {
|
|
101
|
+
return Date.now();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Read + validate ───────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/** Look up a pooled session and validate a reuse request against its frozen
|
|
107
|
+
* config. PURE: no lastUsed bump, no sweep — safe to call speculatively, even
|
|
108
|
+
* outside the session lock (the frozen config is write-once, so a concurrent
|
|
109
|
+
* commit cannot tear the read).
|
|
110
|
+
*
|
|
111
|
+
* - hit → pooled, and {cwd,thinking,tools} match frozen. Returns the live
|
|
112
|
+
* handles + the frozen model id.
|
|
113
|
+
* - miss → not pooled (never inserted, closed, or parent session ended).
|
|
114
|
+
* Caller materializes.
|
|
115
|
+
* - mismatch → pooled but its immutable configuration conflicts with this
|
|
116
|
+
* request. Caller formats the structured diff into an error.
|
|
117
|
+
*
|
|
118
|
+
* lastUsed is bumped by commit() on a successful run, not here — so a checkout
|
|
119
|
+
* that bails (e.g. a resumeFrom conflict at the caller) does not affect stats. */
|
|
120
|
+
export function checkout(
|
|
121
|
+
sessionId: string,
|
|
122
|
+
candidate: ConfigCandidate,
|
|
123
|
+
): CheckoutResult {
|
|
124
|
+
const pooled = agentPool.get(sessionId);
|
|
125
|
+
if (!pooled) return { status: "miss" };
|
|
126
|
+
|
|
127
|
+
const frozen = pooled.config;
|
|
128
|
+
const mismatches: ConfigMismatch[] = [];
|
|
129
|
+
if (frozen.cwd !== candidate.cwd) {
|
|
130
|
+
mismatches.push({
|
|
131
|
+
field: "cwd",
|
|
132
|
+
frozen: frozen.cwd,
|
|
133
|
+
requested: candidate.cwd,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (frozen.thinking !== candidate.thinking) {
|
|
137
|
+
mismatches.push({
|
|
138
|
+
field: "thinking",
|
|
139
|
+
frozen: frozen.thinking,
|
|
140
|
+
requested: candidate.thinking,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Tools are compared as order-independent sets (sorted join), so a reuse that
|
|
144
|
+
// lists the same tools in a different order is not a false mismatch.
|
|
145
|
+
const frozenTools = [...frozen.tools].sort().join(",");
|
|
146
|
+
const requestedTools = [...candidate.tools].sort().join(",");
|
|
147
|
+
if (frozenTools !== requestedTools) {
|
|
148
|
+
mismatches.push({
|
|
149
|
+
field: "tools",
|
|
150
|
+
frozen: frozenTools,
|
|
151
|
+
requested: requestedTools,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
155
|
+
candidate.model &&
|
|
156
|
+
(frozen.model.provider !== candidate.model.provider ||
|
|
157
|
+
frozen.model.id !== candidate.model.id)
|
|
158
|
+
) {
|
|
159
|
+
mismatches.push({
|
|
160
|
+
field: "model",
|
|
161
|
+
frozen: `${frozen.model.provider}/${frozen.model.id}`,
|
|
162
|
+
requested: `${candidate.model.provider}/${candidate.model.id}`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (
|
|
166
|
+
candidate.systemPrompt !== undefined &&
|
|
167
|
+
frozen.systemPrompt !== candidate.systemPrompt
|
|
168
|
+
) {
|
|
169
|
+
// Prompts can be large and sensitive; callers need the conflicting field,
|
|
170
|
+
// not the instruction text in their tool-result context.
|
|
171
|
+
mismatches.push({
|
|
172
|
+
field: "systemPrompt",
|
|
173
|
+
frozen: "<frozen>",
|
|
174
|
+
requested: "<requested>",
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (mismatches.length) return { status: "mismatch", mismatches };
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
status: "hit",
|
|
181
|
+
session: pooled.session,
|
|
182
|
+
sessionManager: pooled.sessionManager,
|
|
183
|
+
sessionFile: pooled.sessionFile,
|
|
184
|
+
modelId: frozen.model.id,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── The sole mutator (besides close) ──────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
/** Record the outcome of a run against a sessionId. Decides insert-vs-recordUse
|
|
191
|
+
* internally by map presence:
|
|
192
|
+
* - present (pool hit) → bump lastUsed, totalTokens, promptCount.
|
|
193
|
+
* - absent (fresh/resume success) → insert with the frozen config.
|
|
194
|
+
*
|
|
195
|
+
* MUST be called inside withSessionLock(sessionId, …) — the map-presence
|
|
196
|
+
* decision is sound only because the lock serializes same-sessionId tasks, so
|
|
197
|
+
* no concurrent commit can race the insert. MUST only be called on run success
|
|
198
|
+
* (insert-only-on-success is caller-gated). Returns true when the session is
|
|
199
|
+
* now pool-owned, false when a fresh session could not be inserted because it
|
|
200
|
+
* lacks the manager/file required by a pooled entry. */
|
|
201
|
+
export function commit(sessionId: string, payload: CommitPayload): boolean {
|
|
202
|
+
const existing = agentPool.get(sessionId);
|
|
203
|
+
if (existing) {
|
|
204
|
+
// Pool hit: session already pooled, just bump stats.
|
|
205
|
+
existing.lastUsed = now();
|
|
206
|
+
existing.totalTokens += payload.tokens;
|
|
207
|
+
existing.promptCount++;
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
// Miss → success: insert. A pooled entry needs a concrete file/manager.
|
|
211
|
+
if (!payload.sessionManager || !payload.sessionFile) return false;
|
|
212
|
+
agentPool.set(sessionId, {
|
|
213
|
+
session: payload.session,
|
|
214
|
+
sessionManager: payload.sessionManager,
|
|
215
|
+
sessionFile: payload.sessionFile,
|
|
216
|
+
config: payload.frozen,
|
|
217
|
+
lastUsed: now(),
|
|
218
|
+
createdAt: now(),
|
|
219
|
+
totalTokens: payload.tokens,
|
|
220
|
+
promptCount: 1,
|
|
221
|
+
});
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Read-only defaults (for task-resolution) ──────────────────────────────
|
|
226
|
+
|
|
227
|
+
/** Frozen config for a pooled session, or undefined if not pooled. Lock-free —
|
|
228
|
+
* safe because the frozen config is write-only at insert and never mutated
|
|
229
|
+
* thereafter (only stats mutate, via commit, and those do not touch the
|
|
230
|
+
* returned object). Used by resolveTasks to default {systemPrompt, model,
|
|
231
|
+
* thinking, tools} for a task that supplies only a sessionId. */
|
|
232
|
+
export function configFor(
|
|
233
|
+
sessionId: string,
|
|
234
|
+
): Readonly<FrozenConfig> | undefined {
|
|
235
|
+
return agentPool.get(sessionId)?.config;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Lock primitive (D1) ───────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
/** Per-session lock — serializes concurrent calls with the same sessionId.
|
|
241
|
+
* Different ids run in parallel. Exported as a primitive so the caller
|
|
242
|
+
* (lifecycle) can bracket the ENTIRE acquire/run/commit flow; checkout/commit
|
|
243
|
+
* do NOT lock internally because their only caller is already inside this
|
|
244
|
+
* bracket. Close is also invoked under this lock, so it never disposes an
|
|
245
|
+
* in-flight prompt. */
|
|
246
|
+
export async function withSessionLock<T>(
|
|
247
|
+
sessionId: string,
|
|
248
|
+
fn: () => Promise<T>,
|
|
249
|
+
): Promise<T> {
|
|
250
|
+
const prev = sessionLocks.get(sessionId);
|
|
251
|
+
let resolve!: () => void;
|
|
252
|
+
const promise = new Promise<void>((r) => {
|
|
253
|
+
resolve = r;
|
|
254
|
+
});
|
|
255
|
+
// Install ourselves BEFORE awaiting — so the next waiter queues behind us,
|
|
256
|
+
// not behind the same predecessor we're waiting on.
|
|
257
|
+
sessionLocks.set(sessionId, promise);
|
|
258
|
+
try {
|
|
259
|
+
if (prev) await prev;
|
|
260
|
+
return await fn();
|
|
261
|
+
} finally {
|
|
262
|
+
resolve();
|
|
263
|
+
// Only clean up if no one queued behind us (our promise is still current).
|
|
264
|
+
// If a waiter installed their own promise, leave it — deleting would
|
|
265
|
+
// clobber their map entry and break the chain.
|
|
266
|
+
if (sessionLocks.get(sessionId) === promise) {
|
|
267
|
+
sessionLocks.delete(sessionId);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Teardown / display ────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
interface AbortOutcome {
|
|
275
|
+
error?: unknown;
|
|
276
|
+
timedOut?: boolean;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Start cancellation without letting a cleanup failure become unhandled while
|
|
280
|
+
* a holder of the per-session lock is still unwinding. A wedged provider/tool
|
|
281
|
+
* cannot keep disposal or parent shutdown waiting forever. */
|
|
282
|
+
function beginAbort(session: AgentSession): Promise<AbortOutcome> {
|
|
283
|
+
return new Promise((resolve) => {
|
|
284
|
+
let settled = false;
|
|
285
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
286
|
+
const finish = (outcome: AbortOutcome): void => {
|
|
287
|
+
if (settled) return;
|
|
288
|
+
settled = true;
|
|
289
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
290
|
+
resolve(outcome);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
timer = setTimeout(() => finish({ timedOut: true }), poolAbortTimeoutMs);
|
|
294
|
+
try {
|
|
295
|
+
void session.abort().then(
|
|
296
|
+
() => finish({}),
|
|
297
|
+
(error: unknown) => finish({ error }),
|
|
298
|
+
);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
finish({ error });
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Close and dispose one pooled session. A caller holds the per-session lock
|
|
306
|
+
* while closing, so abort cannot race a reuse. All cleanup is attempted before
|
|
307
|
+
* an error is surfaced; a removed session is never silently retained. */
|
|
308
|
+
async function closePooledAgentAfterAbort(
|
|
309
|
+
sessionId: string,
|
|
310
|
+
abort: Promise<AbortOutcome>,
|
|
311
|
+
): Promise<boolean> {
|
|
312
|
+
const pooled = agentPool.get(sessionId);
|
|
313
|
+
if (!pooled) return false;
|
|
314
|
+
|
|
315
|
+
const failures: unknown[] = [];
|
|
316
|
+
let abortOutcome: AbortOutcome;
|
|
317
|
+
try {
|
|
318
|
+
abortOutcome = await abort;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
// beginAbort currently normalizes rejection, but keep the teardown seam
|
|
321
|
+
// fail-closed if a future caller supplies a raw abort promise.
|
|
322
|
+
failures.push(error);
|
|
323
|
+
abortOutcome = {};
|
|
324
|
+
}
|
|
325
|
+
if (abortOutcome.error !== undefined) failures.push(abortOutcome.error);
|
|
326
|
+
if (abortOutcome.timedOut) {
|
|
327
|
+
failures.push(
|
|
328
|
+
new Error(
|
|
329
|
+
`Timed out after ${poolAbortTimeoutMs}ms waiting for session '${sessionId}' to abort.`,
|
|
330
|
+
),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
pooled.session.dispose();
|
|
335
|
+
} catch (error) {
|
|
336
|
+
failures.push(error);
|
|
337
|
+
}
|
|
338
|
+
if (agentPool.get(sessionId) === pooled) agentPool.delete(sessionId);
|
|
339
|
+
|
|
340
|
+
if (failures.length) {
|
|
341
|
+
throw new AggregateError(
|
|
342
|
+
failures,
|
|
343
|
+
`Failed to close pooled session '${sessionId}'.`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Abort, dispose, and remove one pooled session. Returns false when the id
|
|
350
|
+
* is already absent; cleanup failures are aggregated after removal. */
|
|
351
|
+
export async function closePooledAgent(sessionId: string): Promise<boolean> {
|
|
352
|
+
const pooled = agentPool.get(sessionId);
|
|
353
|
+
if (!pooled) return false;
|
|
354
|
+
return closePooledAgentAfterAbort(sessionId, beginAbort(pooled.session));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Dispose every live pooled session when the parent Pi session ends. First
|
|
358
|
+
* request cancellation immediately, then acquire each session's lock before
|
|
359
|
+
* disposal. The lock prevents an in-flight lifecycle from committing a live
|
|
360
|
+
* session after shutdown has removed it. Attempts all cleanup before reporting
|
|
361
|
+
* any failures. */
|
|
362
|
+
export async function closeAllPooledAgents(): Promise<void> {
|
|
363
|
+
const aborts = new Map<string, Promise<AbortOutcome>>(
|
|
364
|
+
[...agentPool].map(([sessionId, pooled]) => [
|
|
365
|
+
sessionId,
|
|
366
|
+
beginAbort(pooled.session),
|
|
367
|
+
]),
|
|
368
|
+
);
|
|
369
|
+
const results = await Promise.allSettled(
|
|
370
|
+
[...aborts].map(([sessionId, abort]) =>
|
|
371
|
+
withSessionLock(sessionId, () =>
|
|
372
|
+
closePooledAgentAfterAbort(sessionId, abort),
|
|
373
|
+
),
|
|
374
|
+
),
|
|
375
|
+
);
|
|
376
|
+
const failures = results
|
|
377
|
+
.filter(
|
|
378
|
+
(result): result is PromiseRejectedResult => result.status === "rejected",
|
|
379
|
+
)
|
|
380
|
+
.map((result) => result.reason);
|
|
381
|
+
if (failures.length) {
|
|
382
|
+
throw new AggregateError(
|
|
383
|
+
failures,
|
|
384
|
+
"Failed to close one or more pooled sessions.",
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** List live pooled agents. Sessions remain available until explicit close or
|
|
390
|
+
* parent-session shutdown; idle/age are observability statistics, not expiry. */
|
|
391
|
+
export function listPooledAgents(): string[] {
|
|
392
|
+
const lines: string[] = [];
|
|
393
|
+
if (agentPool.size === 0) return ["_(no active sessions)_"];
|
|
394
|
+
const t = now();
|
|
395
|
+
for (const [id, pooled] of agentPool) {
|
|
396
|
+
const idle = fmtDuration(t - pooled.lastUsed);
|
|
397
|
+
const age = fmtDuration(t - pooled.createdAt);
|
|
398
|
+
lines.push(
|
|
399
|
+
`- **${id}** · ${pooled.promptCount} prompts · ${fmtTokens(pooled.totalTokens)} tokens · idle ${idle} · age ${age} · ${shortenPath(pooled.sessionFile)}`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
return lines;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── Test seam (internal — imported directly by tests, not re-exported) ────
|
|
406
|
+
|
|
407
|
+
/** @internal Override the abort wait in tests without waiting ten seconds. */
|
|
408
|
+
export function _setPoolAbortTimeoutForTesting(
|
|
409
|
+
timeoutMs: number | undefined,
|
|
410
|
+
): void {
|
|
411
|
+
poolAbortTimeoutMs = timeoutMs ?? DEFAULT_POOL_ABORT_TIMEOUT_MS;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** @internal Clear all pool state for test isolation. Tests use fake sessions,
|
|
415
|
+
* so no live resource teardown is needed here. */
|
|
416
|
+
export function _resetPoolForTesting(): void {
|
|
417
|
+
agentPool.clear();
|
|
418
|
+
sessionLocks.clear();
|
|
419
|
+
poolAbortTimeoutMs = DEFAULT_POOL_ABORT_TIMEOUT_MS;
|
|
420
|
+
}
|