@bermudi/pi-delegate 0.1.18 → 0.1.20
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 +64 -15
- package/agents.ts +1 -1
- package/assistant-preview.ts +31 -0
- package/browser-state.ts +250 -0
- package/browser.ts +334 -0
- package/concurrency.ts +7 -0
- package/delegate.ts +8 -0
- package/dispatch.ts +512 -163
- package/extension.ts +65 -28
- package/format.ts +27 -9
- package/host.ts +1 -1
- package/isolated-workspace.ts +154 -8
- package/lifecycle.ts +34 -20
- package/manual.ts +21 -8
- package/package.json +2 -1
- package/parent-context.ts +1 -1
- package/pause.ts +81 -0
- package/pool.ts +492 -428
- package/render-branches.ts +19 -5
- package/render-result.ts +7 -0
- package/runner.ts +55 -1
- package/runtime.ts +36 -0
- package/schema.ts +29 -18
- package/status.ts +38 -11
- package/task-resolution.ts +12 -9
- package/test-harness.ts +81 -0
- package/ticket-format.ts +29 -7
- package/tickets.ts +724 -576
- package/types.ts +33 -0
- package/workspace.ts +58 -27
package/pool.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { fmtDuration, fmtTokens, shortenPath } from "./format.ts";
|
|
|
9
9
|
// ── Public value types (cross the seam) ───────────────────────────────────
|
|
10
10
|
|
|
11
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}.
|
|
12
|
+
* stored it never changes — only stats mutate, and only via {@link SessionPool.commit}.
|
|
13
13
|
* Reuse always validates cwd/thinking/tools and validates an explicitly
|
|
14
14
|
* requested model or base prompt against this frozen configuration. */
|
|
15
15
|
export interface FrozenConfig {
|
|
@@ -51,7 +51,7 @@ export interface ConfigMismatch {
|
|
|
51
51
|
requested: string;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/** Result of {@link checkout}. Discriminated so the caller handles
|
|
54
|
+
/** Result of {@link SessionPool.checkout}. Discriminated so the caller handles
|
|
55
55
|
* hit/miss/mismatch without parsing. checkout is PURE — it does not bump
|
|
56
56
|
* lastUsed (that is commit's job), so a speculative checkout that bails leaves
|
|
57
57
|
* no trace. */
|
|
@@ -67,7 +67,7 @@ export type CheckoutResult =
|
|
|
67
67
|
| { status: "miss" }
|
|
68
68
|
| { status: "mismatch"; mismatches: ConfigMismatch[] };
|
|
69
69
|
|
|
70
|
-
/** Payload for {@link commit}. The caller always assembles the full payload on
|
|
70
|
+
/** Payload for {@link SessionPool.commit}. The caller always assembles the full payload on
|
|
71
71
|
* a successful run; commit decides insert-vs-recordUse by map presence (sound
|
|
72
72
|
* because the per-session lock serializes same-sessionId tasks). */
|
|
73
73
|
export interface CommitPayload {
|
|
@@ -78,8 +78,6 @@ export interface CommitPayload {
|
|
|
78
78
|
tokens: number;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
// ── Internal state (PRIVATE — callers cross the seam, never the Map) ───────
|
|
82
|
-
|
|
83
81
|
interface PooledAgent {
|
|
84
82
|
/** The live AgentSession — reused across prompts for this sessionId. */
|
|
85
83
|
session: AgentSession;
|
|
@@ -95,497 +93,563 @@ interface PooledAgent {
|
|
|
95
93
|
promptCount: number;
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
/** Module-level pool — lives for the entire Pi session. Not exported: callers
|
|
99
|
-
* use checkout/commit/configFor/closePooledAgent/closeAllPooledAgents/listPooledAgents. */
|
|
100
|
-
const agentPool = new Map<string, PooledAgent>();
|
|
101
|
-
|
|
102
|
-
// AgentSession.abort() normally settles promptly, but a provider or tool can
|
|
103
|
-
// ignore cancellation. Cleanup must not make parent-session shutdown hostage to
|
|
104
|
-
// that promise, so close operations use a bounded wait before forced disposal.
|
|
105
|
-
const DEFAULT_POOL_ABORT_TIMEOUT_MS = 10_000;
|
|
106
|
-
let poolAbortTimeoutMs = DEFAULT_POOL_ABORT_TIMEOUT_MS;
|
|
107
|
-
|
|
108
|
-
/** Per-session lock — serializes access to a pooled agent so concurrent
|
|
109
|
-
* delegate calls with the same sessionId queue instead of interleaving. */
|
|
110
|
-
const sessionLocks = new Map<string, Promise<void>>();
|
|
111
|
-
|
|
112
96
|
type PoolShutdownState = "open" | "closing" | "closed";
|
|
113
|
-
let poolState: PoolShutdownState = "open";
|
|
114
|
-
let closePromise: Promise<void> | null = null;
|
|
115
97
|
|
|
116
|
-
|
|
117
|
-
|
|
98
|
+
interface AbortOutcome {
|
|
99
|
+
error?: unknown;
|
|
100
|
+
timedOut?: boolean;
|
|
118
101
|
}
|
|
119
102
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
103
|
+
/** Encapsulated pooled-session state and policy. Each instance owns its own
|
|
104
|
+
* entries, per-session locks, shutdown state/promise, and abort timeout so
|
|
105
|
+
* injected runtimes are fully isolated from one another and from the default
|
|
106
|
+
* module-level pool. */
|
|
107
|
+
export class SessionPool {
|
|
108
|
+
static readonly DEFAULT_ABORT_TIMEOUT_MS = 10_000;
|
|
109
|
+
|
|
110
|
+
private entries = new Map<string, PooledAgent>();
|
|
111
|
+
private readonly sessionLocks = new Map<string, Promise<void>>();
|
|
112
|
+
private state: PoolShutdownState = "open";
|
|
113
|
+
private closePromise: Promise<void> | null = null;
|
|
114
|
+
private abortTimeoutMs = SessionPool.DEFAULT_ABORT_TIMEOUT_MS;
|
|
115
|
+
private quarantineWithoutDisposalOverride:
|
|
116
|
+
((sessionId: string, expectedSession: AgentSession) => boolean) | undefined;
|
|
117
|
+
|
|
118
|
+
private now(): number {
|
|
119
|
+
return Date.now();
|
|
123
120
|
}
|
|
124
|
-
}
|
|
125
121
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function deepFreeze<T>(value: T, seen = new WeakSet<object>()): T {
|
|
132
|
-
if (typeof value !== "object" || value === null || seen.has(value)) {
|
|
133
|
-
return value;
|
|
122
|
+
private assertPoolOpenForNormalWork(): void {
|
|
123
|
+
if (this.state !== "open") {
|
|
124
|
+
throw new Error("Session pool is not accepting new work.");
|
|
125
|
+
}
|
|
134
126
|
}
|
|
135
|
-
seen.add(value);
|
|
136
|
-
const record = value as Record<PropertyKey, unknown>;
|
|
137
|
-
for (const key of Reflect.ownKeys(value)) deepFreeze(record[key], seen);
|
|
138
|
-
return Object.freeze(value);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** Defensive value copy for the capability configuration crossing the pool seam. */
|
|
142
|
-
function cloneFrozenConfig(config: FrozenConfig): FrozenConfig {
|
|
143
|
-
return deepFreeze(structuredClone(config));
|
|
144
|
-
}
|
|
145
127
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
* config. PURE: no lastUsed bump, no sweep — safe to call speculatively, even
|
|
150
|
-
* outside the session lock (the frozen config is write-once, so a concurrent
|
|
151
|
-
* commit cannot tear the read).
|
|
152
|
-
*
|
|
153
|
-
* - hit → pooled, and {cwd,thinking,tools} match frozen. Returns the live
|
|
154
|
-
* handles + the frozen model id.
|
|
155
|
-
* - miss → not pooled (never inserted, closed, or parent session ended).
|
|
156
|
-
* Caller materializes.
|
|
157
|
-
* - mismatch → pooled but its immutable configuration conflicts with this
|
|
158
|
-
* request. Caller formats the structured diff into an error.
|
|
159
|
-
*
|
|
160
|
-
* lastUsed is bumped by recordUse() after a completed pool hit, not here — so
|
|
161
|
-
* a checkout that bails (e.g. a resumeFrom conflict) does not affect stats. */
|
|
162
|
-
export function checkout(
|
|
163
|
-
sessionId: string,
|
|
164
|
-
candidate: ConfigCandidate,
|
|
165
|
-
): CheckoutResult {
|
|
166
|
-
const pooled = agentPool.get(sessionId);
|
|
167
|
-
if (!pooled) return { status: "miss" };
|
|
168
|
-
|
|
169
|
-
const frozen = pooled.config;
|
|
170
|
-
const mismatches: ConfigMismatch[] = [];
|
|
171
|
-
if (frozen.cwd !== candidate.cwd) {
|
|
172
|
-
mismatches.push({
|
|
173
|
-
field: "cwd",
|
|
174
|
-
frozen: frozen.cwd,
|
|
175
|
-
requested: candidate.cwd,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
if (frozen.thinking !== candidate.thinking) {
|
|
179
|
-
mismatches.push({
|
|
180
|
-
field: "thinking",
|
|
181
|
-
frozen: frozen.thinking,
|
|
182
|
-
requested: candidate.thinking,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
// Tools are compared as order-independent sets (sorted join), so a reuse that
|
|
186
|
-
// lists the same tools in a different order is not a false mismatch.
|
|
187
|
-
const frozenTools = [...frozen.tools].sort().join(",");
|
|
188
|
-
const requestedTools = [...candidate.tools].sort().join(",");
|
|
189
|
-
if (frozenTools !== requestedTools) {
|
|
190
|
-
mismatches.push({
|
|
191
|
-
field: "tools",
|
|
192
|
-
frozen: frozenTools,
|
|
193
|
-
requested: requestedTools,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
if (
|
|
197
|
-
candidate.model &&
|
|
198
|
-
(frozen.model.provider !== candidate.model.provider ||
|
|
199
|
-
frozen.model.id !== candidate.model.id)
|
|
200
|
-
) {
|
|
201
|
-
mismatches.push({
|
|
202
|
-
field: "model",
|
|
203
|
-
frozen: `${frozen.model.provider}/${frozen.model.id}`,
|
|
204
|
-
requested: `${candidate.model.provider}/${candidate.model.id}`,
|
|
205
|
-
});
|
|
128
|
+
private waitForActiveSessionLocks(): Promise<void> {
|
|
129
|
+
const locks = [...this.sessionLocks.values()];
|
|
130
|
+
return Promise.all(locks).then(() => undefined);
|
|
206
131
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
if (frozen.providerExtensions !== candidate.providerExtensions) {
|
|
220
|
-
// This field is derived from configured package sources, which may contain
|
|
221
|
-
// credentials. Never expose either the sources or their digest through the
|
|
222
|
-
// public checkout result; a digest can still enable dictionary guessing.
|
|
223
|
-
mismatches.push({
|
|
224
|
-
field: "providerExtensions",
|
|
225
|
-
frozen: "<redacted>",
|
|
226
|
-
requested: "<redacted>",
|
|
227
|
-
});
|
|
132
|
+
|
|
133
|
+
private deepFreeze<T>(value: T, seen = new WeakSet<object>()): T {
|
|
134
|
+
if (typeof value !== "object" || value === null || seen.has(value)) {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
seen.add(value);
|
|
138
|
+
const record = value as Record<PropertyKey, unknown>;
|
|
139
|
+
for (const key of Reflect.ownKeys(value))
|
|
140
|
+
this.deepFreeze(record[key], seen);
|
|
141
|
+
return Object.freeze(value);
|
|
228
142
|
}
|
|
229
|
-
if (mismatches.length) return { status: "mismatch", mismatches };
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
status: "hit",
|
|
233
|
-
session: pooled.session,
|
|
234
|
-
sessionManager: pooled.sessionManager,
|
|
235
|
-
sessionFile: pooled.sessionFile,
|
|
236
|
-
modelId: frozen.model.id,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
143
|
|
|
240
|
-
|
|
144
|
+
/** Defensive value copy for the capability configuration crossing the pool seam. */
|
|
145
|
+
private cloneFrozenConfig(config: FrozenConfig): FrozenConfig {
|
|
146
|
+
return this.deepFreeze(structuredClone(config));
|
|
147
|
+
}
|
|
241
148
|
|
|
242
|
-
/**
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
149
|
+
/** Look up a pooled session and validate a reuse request against its frozen
|
|
150
|
+
* config. PURE: no lastUsed bump, no sweep — safe to call speculatively, even
|
|
151
|
+
* outside the session lock (the frozen config is write-once, so a concurrent
|
|
152
|
+
* commit cannot tear the read).
|
|
153
|
+
*
|
|
154
|
+
* - hit → pooled, and {cwd,thinking,tools} match frozen. Returns the live
|
|
155
|
+
* handles + the frozen model id.
|
|
156
|
+
* - miss → not pooled (never inserted, closed, or parent session ended).
|
|
157
|
+
* Caller materializes.
|
|
158
|
+
* - mismatch → pooled but its immutable configuration conflicts with this
|
|
159
|
+
* request. Caller formats the structured diff into an error.
|
|
160
|
+
*
|
|
161
|
+
* lastUsed is bumped by recordUse() after a completed pool hit, not here — so
|
|
162
|
+
* a checkout that bails (e.g. a resumeFrom conflict) does not affect stats. */
|
|
163
|
+
checkout(sessionId: string, candidate: ConfigCandidate): CheckoutResult {
|
|
164
|
+
const pooled = this.entries.get(sessionId);
|
|
165
|
+
if (!pooled) return { status: "miss" };
|
|
166
|
+
|
|
167
|
+
const frozen = pooled.config;
|
|
168
|
+
const mismatches: ConfigMismatch[] = [];
|
|
169
|
+
if (frozen.cwd !== candidate.cwd) {
|
|
170
|
+
mismatches.push({
|
|
171
|
+
field: "cwd",
|
|
172
|
+
frozen: frozen.cwd,
|
|
173
|
+
requested: candidate.cwd,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (frozen.thinking !== candidate.thinking) {
|
|
177
|
+
mismatches.push({
|
|
178
|
+
field: "thinking",
|
|
179
|
+
frozen: frozen.thinking,
|
|
180
|
+
requested: candidate.thinking,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// Tools are compared as order-independent sets (sorted join), so a reuse that
|
|
184
|
+
// lists the same tools in a different order is not a false mismatch.
|
|
185
|
+
const frozenTools = [...frozen.tools].sort().join(",");
|
|
186
|
+
const requestedTools = [...candidate.tools].sort().join(",");
|
|
187
|
+
if (frozenTools !== requestedTools) {
|
|
188
|
+
mismatches.push({
|
|
189
|
+
field: "tools",
|
|
190
|
+
frozen: frozenTools,
|
|
191
|
+
requested: requestedTools,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (
|
|
195
|
+
candidate.model &&
|
|
196
|
+
(frozen.model.provider !== candidate.model.provider ||
|
|
197
|
+
frozen.model.id !== candidate.model.id)
|
|
198
|
+
) {
|
|
199
|
+
mismatches.push({
|
|
200
|
+
field: "model",
|
|
201
|
+
frozen: `${frozen.model.provider}/${frozen.model.id}`,
|
|
202
|
+
requested: `${candidate.model.provider}/${candidate.model.id}`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (
|
|
206
|
+
candidate.systemPrompt !== undefined &&
|
|
207
|
+
frozen.systemPrompt !== candidate.systemPrompt
|
|
208
|
+
) {
|
|
209
|
+
// Prompts can be large and sensitive; callers need the conflicting field,
|
|
210
|
+
// not the instruction text in their tool-result context.
|
|
211
|
+
mismatches.push({
|
|
212
|
+
field: "systemPrompt",
|
|
213
|
+
frozen: "<frozen>",
|
|
214
|
+
requested: "<requested>",
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (frozen.providerExtensions !== candidate.providerExtensions) {
|
|
218
|
+
// This field is derived from configured package sources, which may contain
|
|
219
|
+
// credentials. Never expose either the sources or their digest through the
|
|
220
|
+
// public checkout result; a digest can still enable dictionary guessing.
|
|
221
|
+
mismatches.push({
|
|
222
|
+
field: "providerExtensions",
|
|
223
|
+
frozen: "<redacted>",
|
|
224
|
+
requested: "<redacted>",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (mismatches.length) return { status: "mismatch", mismatches };
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
status: "hit",
|
|
231
|
+
session: pooled.session,
|
|
232
|
+
sessionManager: pooled.sessionManager,
|
|
233
|
+
sessionFile: pooled.sessionFile,
|
|
234
|
+
modelId: frozen.model.id,
|
|
235
|
+
};
|
|
256
236
|
}
|
|
257
237
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
238
|
+
/**
|
|
239
|
+
* Insert the first successful prompt for a fresh/resumed session into the pool.
|
|
240
|
+
*
|
|
241
|
+
* MUST be called inside withSessionLock(sessionId, …) and only when this run
|
|
242
|
+
* should transfer ownership from lifecycle to the pool. Returns true when the
|
|
243
|
+
* session is now pool-owned, false when insertion is blocked (shutdown) or
|
|
244
|
+
* impossible (missing manager/file).
|
|
245
|
+
*/
|
|
246
|
+
commit(sessionId: string, payload: CommitPayload): boolean {
|
|
247
|
+
// Shutdown-aware policy: once shutdown has started, inserting a new pool entry
|
|
248
|
+
// is unsafe. The lifecycle handles the still-owned session (abort/cleanup) and
|
|
249
|
+
// should dispose it instead of inserting it after the barrier begins.
|
|
250
|
+
if (this.state !== "open") {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
274
253
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
254
|
+
// Miss → success: insert. A pooled entry needs a concrete file/manager.
|
|
255
|
+
if (!payload.sessionManager || !payload.sessionFile) return false;
|
|
256
|
+
|
|
257
|
+
if (this.entries.has(sessionId)) return false;
|
|
258
|
+
this.entries.set(sessionId, {
|
|
259
|
+
session: payload.session,
|
|
260
|
+
sessionManager: payload.sessionManager,
|
|
261
|
+
sessionFile: payload.sessionFile,
|
|
262
|
+
config: this.cloneFrozenConfig(payload.frozen),
|
|
263
|
+
lastUsed: this.now(),
|
|
264
|
+
createdAt: this.now(),
|
|
265
|
+
totalTokens: payload.tokens,
|
|
266
|
+
promptCount: 1,
|
|
267
|
+
});
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
282
270
|
|
|
283
|
-
existing.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
271
|
+
/** Record a completed run against an existing pooled session. This is separate
|
|
272
|
+
* from commit() so lifecycle can distinguish ownership transfer from hit
|
|
273
|
+
* accounting. Returns true only when the sessionId is currently pooled. */
|
|
274
|
+
recordUse(sessionId: string, tokens: number): boolean {
|
|
275
|
+
const existing = this.entries.get(sessionId);
|
|
276
|
+
if (!existing) return false;
|
|
277
|
+
|
|
278
|
+
existing.lastUsed = this.now();
|
|
279
|
+
existing.totalTokens += tokens;
|
|
280
|
+
existing.promptCount++;
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
288
283
|
|
|
289
|
-
/** Remove an exact live session from reuse without aborting or disposing it.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
): boolean {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
284
|
+
/** Remove an exact live session from reuse without aborting or disposing it.
|
|
285
|
+
* Lifecycle uses this only after runner reports quiescence abandonment: any
|
|
286
|
+
* ordinary pool close would race provider/extension work that may still be
|
|
287
|
+
* running. The caller retains the detached session until its background safety
|
|
288
|
+
* promise resolves. */
|
|
289
|
+
quarantinePooledAgentWithoutDisposal(
|
|
290
|
+
sessionId: string,
|
|
291
|
+
expectedSession: AgentSession,
|
|
292
|
+
): boolean {
|
|
293
|
+
if (this.quarantineWithoutDisposalOverride) {
|
|
294
|
+
return this.quarantineWithoutDisposalOverride(sessionId, expectedSession);
|
|
295
|
+
}
|
|
296
|
+
const existing = this.entries.get(sessionId);
|
|
297
|
+
if (!existing || existing.session !== expectedSession) return false;
|
|
298
|
+
this.entries.delete(sessionId);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
303
301
|
|
|
304
|
-
|
|
302
|
+
/** @internal Test-only override for the quarantine-without-disposal path. */
|
|
303
|
+
_setQuarantinePooledAgentWithoutDisposalForTesting(
|
|
304
|
+
override:
|
|
305
|
+
| ((sessionId: string, expectedSession: AgentSession) => boolean)
|
|
306
|
+
| undefined,
|
|
307
|
+
): void {
|
|
308
|
+
this.quarantineWithoutDisposalOverride = override;
|
|
309
|
+
}
|
|
305
310
|
|
|
306
|
-
/** Frozen config for a pooled session, or undefined if not pooled. Lock-free —
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
):
|
|
314
|
-
|
|
315
|
-
return config ? cloneFrozenConfig(config) : undefined;
|
|
316
|
-
}
|
|
311
|
+
/** Frozen config for a pooled session, or undefined if not pooled. Lock-free —
|
|
312
|
+
* safe because the stored config is a deeply frozen defensive copy. Callers
|
|
313
|
+
* receive another frozen copy so values crossing the pool seam cannot mutate
|
|
314
|
+
* its capability contract. Used by resolveTasks to default {systemPrompt,
|
|
315
|
+
* model, thinking, tools} for a task that supplies only a sessionId. */
|
|
316
|
+
configFor(sessionId: string): Readonly<FrozenConfig> | undefined {
|
|
317
|
+
const config = this.entries.get(sessionId)?.config;
|
|
318
|
+
return config ? this.cloneFrozenConfig(config) : undefined;
|
|
319
|
+
}
|
|
317
320
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
fn: () => Promise<T>,
|
|
334
|
-
): Promise<T> {
|
|
335
|
-
assertPoolOpenForNormalWork();
|
|
336
|
-
return withSessionLockInternal(sessionId, fn);
|
|
337
|
-
}
|
|
321
|
+
/** Per-session lock — serializes concurrent calls with the same sessionId.
|
|
322
|
+
* Different ids run in parallel. Exported as a primitive so the caller
|
|
323
|
+
* (lifecycle) can bracket the ENTIRE acquire/run/commit flow; checkout/commit
|
|
324
|
+
* do NOT lock internally because their only caller is already inside this
|
|
325
|
+
* bracket. Close is also invoked under this lock, so it never disposes an
|
|
326
|
+
* in-flight prompt.
|
|
327
|
+
*
|
|
328
|
+
* External callers must go through this exported path only while the pool is
|
|
329
|
+
* open. Internal callers that are already synchronized by a higher-level lock
|
|
330
|
+
* can use `withSessionLockInternal`.
|
|
331
|
+
*/
|
|
332
|
+
withSessionLock<T>(sessionId: string, fn: () => Promise<T>): Promise<T> {
|
|
333
|
+
this.assertPoolOpenForNormalWork();
|
|
334
|
+
return this.withSessionLockInternal(sessionId, fn);
|
|
335
|
+
}
|
|
338
336
|
|
|
339
|
-
/** Internal lock variant that does not reject during shutdown. Use only from
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
337
|
+
/** Internal lock variant that does not reject during shutdown. Use only from
|
|
338
|
+
* lifecycle/control paths that already account for pool shutdown state. */
|
|
339
|
+
private async withSessionLockInternal<T>(
|
|
340
|
+
sessionId: string,
|
|
341
|
+
fn: () => Promise<T>,
|
|
342
|
+
): Promise<T> {
|
|
343
|
+
const prev = this.sessionLocks.get(sessionId);
|
|
344
|
+
let resolve!: () => void;
|
|
345
|
+
const promise = new Promise<void>((r) => {
|
|
346
|
+
resolve = r;
|
|
347
|
+
});
|
|
348
|
+
// Install ourselves BEFORE awaiting — so the next waiter queues behind us,
|
|
349
|
+
// not behind the same predecessor we’re waiting on.
|
|
350
|
+
this.sessionLocks.set(sessionId, promise);
|
|
351
|
+
try {
|
|
352
|
+
if (prev) await prev;
|
|
353
|
+
return await fn();
|
|
354
|
+
} finally {
|
|
355
|
+
resolve();
|
|
356
|
+
// Only clean up if no one queued behind us (our promise is still current).
|
|
357
|
+
// If a waiter installed their own promise, leave it — deleting would
|
|
358
|
+
// clobber their map entry and break the chain.
|
|
359
|
+
if (this.sessionLocks.get(sessionId) === promise) {
|
|
360
|
+
this.sessionLocks.delete(sessionId);
|
|
361
|
+
}
|
|
364
362
|
}
|
|
365
363
|
}
|
|
366
|
-
}
|
|
367
364
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
365
|
+
/** Start cancellation without letting a cleanup failure become unhandled while
|
|
366
|
+
* a holder of the per-session lock is still unwinding. A wedged provider/tool
|
|
367
|
+
* cannot keep disposal or parent shutdown waiting forever. */
|
|
368
|
+
private beginAbort(session: AgentSession): Promise<AbortOutcome> {
|
|
369
|
+
return new Promise((resolve) => {
|
|
370
|
+
let settled = false;
|
|
371
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
372
|
+
const finish = (outcome: AbortOutcome): void => {
|
|
373
|
+
if (settled) return;
|
|
374
|
+
settled = true;
|
|
375
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
376
|
+
resolve(outcome);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
timer = setTimeout(() => finish({ timedOut: true }), this.abortTimeoutMs);
|
|
380
|
+
try {
|
|
381
|
+
void session.abort().then(
|
|
382
|
+
() => finish({}),
|
|
383
|
+
(error: unknown) => finish({ error }),
|
|
384
|
+
);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
finish({ error });
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
388
390
|
|
|
389
|
-
|
|
391
|
+
/** Close and dispose one pooled session. A caller holds the per-session lock
|
|
392
|
+
* while closing, so abort cannot race a reuse. All cleanup is attempted before
|
|
393
|
+
* an error is surfaced; a removed session is never silently retained. */
|
|
394
|
+
private async closePooledAgentAfterAbort(
|
|
395
|
+
sessionId: string,
|
|
396
|
+
abort: Promise<AbortOutcome>,
|
|
397
|
+
): Promise<boolean> {
|
|
398
|
+
const pooled = this.entries.get(sessionId);
|
|
399
|
+
if (!pooled) return false;
|
|
400
|
+
|
|
401
|
+
const failures: unknown[] = [];
|
|
402
|
+
let abortOutcome: AbortOutcome;
|
|
390
403
|
try {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
404
|
+
abortOutcome = await abort;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
// beginAbort currently normalizes rejection, but keep the teardown seam
|
|
407
|
+
// fail-closed if a future caller supplies a raw abort promise.
|
|
408
|
+
failures.push(error);
|
|
409
|
+
abortOutcome = {};
|
|
410
|
+
}
|
|
411
|
+
if (abortOutcome.error !== undefined) failures.push(abortOutcome.error);
|
|
412
|
+
if (abortOutcome.timedOut) {
|
|
413
|
+
failures.push(
|
|
414
|
+
new Error(
|
|
415
|
+
`Timed out after ${this.abortTimeoutMs}ms waiting for session '${sessionId}' to abort.`,
|
|
416
|
+
),
|
|
394
417
|
);
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
pooled.session.dispose();
|
|
395
421
|
} catch (error) {
|
|
396
|
-
|
|
422
|
+
failures.push(error);
|
|
397
423
|
}
|
|
398
|
-
|
|
399
|
-
}
|
|
424
|
+
if (this.entries.get(sessionId) === pooled) this.entries.delete(sessionId);
|
|
400
425
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const pooled = agentPool.get(sessionId);
|
|
409
|
-
if (!pooled) return false;
|
|
410
|
-
|
|
411
|
-
const failures: unknown[] = [];
|
|
412
|
-
let abortOutcome: AbortOutcome;
|
|
413
|
-
try {
|
|
414
|
-
abortOutcome = await abort;
|
|
415
|
-
} catch (error) {
|
|
416
|
-
// beginAbort currently normalizes rejection, but keep the teardown seam
|
|
417
|
-
// fail-closed if a future caller supplies a raw abort promise.
|
|
418
|
-
failures.push(error);
|
|
419
|
-
abortOutcome = {};
|
|
426
|
+
if (failures.length) {
|
|
427
|
+
throw new AggregateError(
|
|
428
|
+
failures,
|
|
429
|
+
`Failed to close pooled session '${sessionId}'.`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
return true;
|
|
420
433
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
434
|
+
|
|
435
|
+
/** Internal close helper for callers that already own (or are obtaining) the
|
|
436
|
+
* session lock. */
|
|
437
|
+
private closePooledAgentAfterLock(sessionId: string): Promise<boolean> {
|
|
438
|
+
const pooled = this.entries.get(sessionId);
|
|
439
|
+
if (!pooled) return Promise.resolve(false);
|
|
440
|
+
return this.closePooledAgentAfterAbort(
|
|
441
|
+
sessionId,
|
|
442
|
+
this.beginAbort(pooled.session),
|
|
427
443
|
);
|
|
428
444
|
}
|
|
429
|
-
try {
|
|
430
|
-
pooled.session.dispose();
|
|
431
|
-
} catch (error) {
|
|
432
|
-
failures.push(error);
|
|
433
|
-
}
|
|
434
|
-
if (agentPool.get(sessionId) === pooled) agentPool.delete(sessionId);
|
|
435
445
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
`Failed to close pooled session '${sessionId}'.`,
|
|
440
|
-
);
|
|
446
|
+
/** @internal Close while the caller already owns the per-session lock. */
|
|
447
|
+
closePooledAgentWithoutLock(sessionId: string): Promise<boolean> {
|
|
448
|
+
return this.closePooledAgentAfterLock(sessionId);
|
|
441
449
|
}
|
|
442
|
-
return true;
|
|
443
|
-
}
|
|
444
450
|
|
|
445
|
-
/**
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
451
|
+
/** Abort, dispose, and remove one pooled session. Returns false when the id
|
|
452
|
+
* is already absent; cleanup failures are aggregated after removal.
|
|
453
|
+
* During shutdown or after close, this waits for shutdown to finish and
|
|
454
|
+
* returns `false` instead of throwing.
|
|
455
|
+
*/
|
|
456
|
+
async closePooledAgent(sessionId: string): Promise<boolean> {
|
|
457
|
+
if (this.state !== "open") {
|
|
458
|
+
const existing = this.closePromise;
|
|
459
|
+
if (existing) {
|
|
460
|
+
try {
|
|
461
|
+
await existing;
|
|
462
|
+
} catch {
|
|
463
|
+
// Keep public close idempotent during and after shutdown.
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
452
468
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
sessionId: string,
|
|
456
|
-
): Promise<boolean> {
|
|
457
|
-
return closePooledAgentAfterLock(sessionId);
|
|
458
|
-
}
|
|
469
|
+
let awaitShutdown: Promise<void> | undefined;
|
|
470
|
+
let closed = false;
|
|
459
471
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
472
|
+
await this.withSessionLockInternal(sessionId, async () => {
|
|
473
|
+
// A shutdown can begin while this call waits for its lock. Avoid waiting
|
|
474
|
+
// for closePromise inside this lock: do that work after releasing it, so
|
|
475
|
+
// closeAll can acquire the lock and dispose the same session.
|
|
476
|
+
if (this.state !== "open") {
|
|
477
|
+
awaitShutdown = this.closePromise ?? undefined;
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
closed = await this.closePooledAgentAfterLock(sessionId);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
if (awaitShutdown) {
|
|
469
484
|
try {
|
|
470
|
-
await
|
|
485
|
+
await awaitShutdown;
|
|
471
486
|
} catch {
|
|
472
487
|
// Keep public close idempotent during and after shutdown.
|
|
473
488
|
}
|
|
474
489
|
}
|
|
475
|
-
return false;
|
|
476
|
-
}
|
|
477
490
|
|
|
478
|
-
|
|
479
|
-
|
|
491
|
+
return closed;
|
|
492
|
+
}
|
|
480
493
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
494
|
+
/** Dispose every live pooled session when the parent Pi session ends. First
|
|
495
|
+
* request cancellation immediately, then wait for all in-flight session locks,
|
|
496
|
+
* then acquire each remaining session's lock before disposal. Attempts are
|
|
497
|
+
* executed for all sessions before reporting failures. Idempotent callers share
|
|
498
|
+
* the same completion promise, including failures.
|
|
499
|
+
*/
|
|
500
|
+
async closeAllPooledAgents(): Promise<void> {
|
|
501
|
+
if (this.closePromise) {
|
|
502
|
+
return this.closePromise;
|
|
488
503
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
try {
|
|
494
|
-
await awaitShutdown;
|
|
495
|
-
} catch {
|
|
496
|
-
// Keep public close idempotent during and after shutdown.
|
|
504
|
+
if (this.state === "closed") {
|
|
505
|
+
const completed = Promise.resolve();
|
|
506
|
+
this.closePromise = completed;
|
|
507
|
+
return completed;
|
|
497
508
|
}
|
|
509
|
+
|
|
510
|
+
this.state = "closing";
|
|
511
|
+
|
|
512
|
+
const completion = (async () => {
|
|
513
|
+
const aborts = new Map<string, Promise<AbortOutcome>>(
|
|
514
|
+
[...this.entries].map(([sessionId, pooled]) => [
|
|
515
|
+
sessionId,
|
|
516
|
+
this.beginAbort(pooled.session),
|
|
517
|
+
]),
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
await this.waitForActiveSessionLocks();
|
|
521
|
+
|
|
522
|
+
const results = await Promise.allSettled(
|
|
523
|
+
[...aborts].map(([sessionId, abort]) =>
|
|
524
|
+
this.withSessionLockInternal(sessionId, () =>
|
|
525
|
+
this.closePooledAgentAfterAbort(sessionId, abort),
|
|
526
|
+
),
|
|
527
|
+
),
|
|
528
|
+
);
|
|
529
|
+
const failures = results
|
|
530
|
+
.filter(
|
|
531
|
+
(result): result is PromiseRejectedResult =>
|
|
532
|
+
result.status === "rejected",
|
|
533
|
+
)
|
|
534
|
+
.map((result) => result.reason);
|
|
535
|
+
if (failures.length) {
|
|
536
|
+
throw new AggregateError(
|
|
537
|
+
failures,
|
|
538
|
+
"Failed to close one or more pooled sessions.",
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
})();
|
|
542
|
+
|
|
543
|
+
this.closePromise = completion.finally(() => {
|
|
544
|
+
this.state = "closed";
|
|
545
|
+
});
|
|
546
|
+
return this.closePromise;
|
|
498
547
|
}
|
|
499
548
|
|
|
500
|
-
|
|
501
|
-
|
|
549
|
+
/** List live pooled agents. Sessions remain available until explicit close or
|
|
550
|
+
* parent-session shutdown; idle/age are observability statistics, not expiry. */
|
|
551
|
+
listPooledAgents(): string[] {
|
|
552
|
+
const lines: string[] = [];
|
|
553
|
+
if (this.entries.size === 0) return ["_(no active sessions)_"];
|
|
554
|
+
const t = this.now();
|
|
555
|
+
for (const [id, pooled] of this.entries) {
|
|
556
|
+
const idle = fmtDuration(t - pooled.lastUsed);
|
|
557
|
+
const age = fmtDuration(t - pooled.createdAt);
|
|
558
|
+
lines.push(
|
|
559
|
+
`- **${id}** · ${pooled.promptCount} prompts · ${fmtTokens(pooled.totalTokens)} tokens · idle ${idle} · age ${age} · ${shortenPath(pooled.sessionFile)}`,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
return lines;
|
|
563
|
+
}
|
|
502
564
|
|
|
503
|
-
/**
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
565
|
+
/** @internal Clear all pool state for test isolation. Tests use fake sessions,
|
|
566
|
+
* so no live resource teardown is needed here. */
|
|
567
|
+
resetForTesting(): void {
|
|
568
|
+
this.entries.clear();
|
|
569
|
+
this.sessionLocks.clear();
|
|
570
|
+
this.state = "open";
|
|
571
|
+
this.closePromise = null;
|
|
572
|
+
this.abortTimeoutMs = SessionPool.DEFAULT_ABORT_TIMEOUT_MS;
|
|
573
|
+
this.quarantineWithoutDisposalOverride = undefined;
|
|
512
574
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
575
|
+
|
|
576
|
+
/** @internal Override the abort wait in tests without waiting ten seconds. */
|
|
577
|
+
setAbortTimeoutForTesting(timeoutMs: number | undefined): void {
|
|
578
|
+
this.abortTimeoutMs = timeoutMs ?? SessionPool.DEFAULT_ABORT_TIMEOUT_MS;
|
|
517
579
|
}
|
|
580
|
+
}
|
|
518
581
|
|
|
519
|
-
|
|
582
|
+
// ── Default module-level pool and compatibility wrappers ──────────────────
|
|
520
583
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
beginAbort(pooled.session),
|
|
526
|
-
]),
|
|
527
|
-
);
|
|
584
|
+
/** Default pool instance used by the module-level wrappers and the default
|
|
585
|
+
* {@link getDefaultDelegateRuntime}. Exported so callers can build a
|
|
586
|
+
* {@link DelegateRuntime} that shares the same default pool. */
|
|
587
|
+
export const defaultSessionPool = new SessionPool();
|
|
528
588
|
|
|
529
|
-
|
|
589
|
+
export function checkout(
|
|
590
|
+
sessionId: string,
|
|
591
|
+
candidate: ConfigCandidate,
|
|
592
|
+
): CheckoutResult {
|
|
593
|
+
return defaultSessionPool.checkout(sessionId, candidate);
|
|
594
|
+
}
|
|
530
595
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
closePooledAgentAfterAbort(sessionId, abort),
|
|
535
|
-
),
|
|
536
|
-
),
|
|
537
|
-
);
|
|
538
|
-
const failures = results
|
|
539
|
-
.filter(
|
|
540
|
-
(result): result is PromiseRejectedResult =>
|
|
541
|
-
result.status === "rejected",
|
|
542
|
-
)
|
|
543
|
-
.map((result) => result.reason);
|
|
544
|
-
if (failures.length) {
|
|
545
|
-
throw new AggregateError(
|
|
546
|
-
failures,
|
|
547
|
-
"Failed to close one or more pooled sessions.",
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
})();
|
|
596
|
+
export function commit(sessionId: string, payload: CommitPayload): boolean {
|
|
597
|
+
return defaultSessionPool.commit(sessionId, payload);
|
|
598
|
+
}
|
|
551
599
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
600
|
+
export function recordUse(sessionId: string, tokens: number): boolean {
|
|
601
|
+
return defaultSessionPool.recordUse(sessionId, tokens);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export function configFor(
|
|
605
|
+
sessionId: string,
|
|
606
|
+
): Readonly<FrozenConfig> | undefined {
|
|
607
|
+
return defaultSessionPool.configFor(sessionId);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function withSessionLock<T>(
|
|
611
|
+
sessionId: string,
|
|
612
|
+
fn: () => Promise<T>,
|
|
613
|
+
): Promise<T> {
|
|
614
|
+
return defaultSessionPool.withSessionLock(sessionId, fn);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export async function closePooledAgent(sessionId: string): Promise<boolean> {
|
|
618
|
+
return defaultSessionPool.closePooledAgent(sessionId);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export async function closeAllPooledAgents(): Promise<void> {
|
|
622
|
+
return defaultSessionPool.closeAllPooledAgents();
|
|
556
623
|
}
|
|
557
624
|
|
|
558
|
-
/** List live pooled agents. Sessions remain available until explicit close or
|
|
559
|
-
* parent-session shutdown; idle/age are observability statistics, not expiry. */
|
|
560
625
|
export function listPooledAgents(): string[] {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
626
|
+
return defaultSessionPool.listPooledAgents();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export function _quarantinePooledAgentWithoutDisposal(
|
|
630
|
+
sessionId: string,
|
|
631
|
+
expectedSession: AgentSession,
|
|
632
|
+
): boolean {
|
|
633
|
+
return defaultSessionPool.quarantinePooledAgentWithoutDisposal(
|
|
634
|
+
sessionId,
|
|
635
|
+
expectedSession,
|
|
636
|
+
);
|
|
572
637
|
}
|
|
573
638
|
|
|
574
|
-
|
|
639
|
+
export function _closePooledAgentWithoutLock(
|
|
640
|
+
sessionId: string,
|
|
641
|
+
): Promise<boolean> {
|
|
642
|
+
return defaultSessionPool.closePooledAgentWithoutLock(sessionId);
|
|
643
|
+
}
|
|
575
644
|
|
|
576
645
|
/** @internal Override the abort wait in tests without waiting ten seconds. */
|
|
577
646
|
export function _setPoolAbortTimeoutForTesting(
|
|
578
647
|
timeoutMs: number | undefined,
|
|
579
648
|
): void {
|
|
580
|
-
|
|
649
|
+
defaultSessionPool.setAbortTimeoutForTesting(timeoutMs);
|
|
581
650
|
}
|
|
582
651
|
|
|
583
|
-
/** @internal Clear all pool state for test isolation.
|
|
584
|
-
* so no live resource teardown is needed here. */
|
|
652
|
+
/** @internal Clear all default pool state for test isolation. */
|
|
585
653
|
export function _resetPoolForTesting(): void {
|
|
586
|
-
|
|
587
|
-
sessionLocks.clear();
|
|
588
|
-
poolState = "open";
|
|
589
|
-
closePromise = null;
|
|
590
|
-
poolAbortTimeoutMs = DEFAULT_POOL_ABORT_TIMEOUT_MS;
|
|
654
|
+
defaultSessionPool.resetForTesting();
|
|
591
655
|
}
|