@bermudi/pi-delegate 0.1.2 → 0.1.4
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 +27 -1
- package/concurrency.ts +20 -3
- package/config.ts +62 -1
- package/delegate.ts +4 -0
- package/dispatch.ts +94 -15
- package/extension.ts +292 -61
- package/format.ts +5 -1
- package/host-compat.ts +1 -0
- package/lifecycle.ts +235 -25
- package/manual.ts +3 -2
- package/package.json +1 -1
- package/schema.ts +16 -2
- package/task-resolution.ts +6 -0
- package/telemetry.ts +743 -0
- package/tickets.ts +34 -10
- package/types.ts +22 -0
- package/usage.ts +19 -0
- package/workspace.ts +672 -0
package/extension.ts
CHANGED
|
@@ -30,10 +30,87 @@ import {
|
|
|
30
30
|
notifyActiveTicketsOnSettled,
|
|
31
31
|
syncDelegateStatus,
|
|
32
32
|
} from "./status.ts";
|
|
33
|
+
import {
|
|
34
|
+
beginCall,
|
|
35
|
+
closeTelemetry,
|
|
36
|
+
getTelemetryGeneration,
|
|
37
|
+
prepareTelemetryForSession,
|
|
38
|
+
sealTelemetryWrites,
|
|
39
|
+
} from "./telemetry.ts";
|
|
33
40
|
import type { DelegateArguments } from "./types.ts";
|
|
34
41
|
|
|
42
|
+
const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 10_000;
|
|
43
|
+
let shutdownDrainTimeoutMs = DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS;
|
|
44
|
+
|
|
45
|
+
type ShutdownDrainResult =
|
|
46
|
+
{ drained: true; failures: unknown[] } | { drained: false; failures: [] };
|
|
47
|
+
|
|
48
|
+
/** Wait for shutdown workers without allowing a stuck provider/tool to hold
|
|
49
|
+
* Pi's reload or exit hostage. The allSettled promise is intentionally left
|
|
50
|
+
* attached after timeout so a late rejection cannot become unhandled. */
|
|
51
|
+
export async function drainAsyncTickets(
|
|
52
|
+
completions: readonly Promise<void>[],
|
|
53
|
+
timeoutMs = shutdownDrainTimeoutMs,
|
|
54
|
+
): Promise<ShutdownDrainResult> {
|
|
55
|
+
if (!completions.length) return { drained: true, failures: [] };
|
|
56
|
+
|
|
57
|
+
const drain = Promise.allSettled(completions).then((results) => ({
|
|
58
|
+
drained: true as const,
|
|
59
|
+
failures: results
|
|
60
|
+
.filter(
|
|
61
|
+
(result): result is PromiseRejectedResult =>
|
|
62
|
+
result.status === "rejected",
|
|
63
|
+
)
|
|
64
|
+
.map((result) => result.reason),
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
68
|
+
const timeout = new Promise<ShutdownDrainResult>((resolve) => {
|
|
69
|
+
timer = setTimeout(
|
|
70
|
+
() => resolve({ drained: false, failures: [] }),
|
|
71
|
+
timeoutMs,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
const result = await Promise.race([drain, timeout]);
|
|
75
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Wait for another shutdown cleanup operation without allowing a stuck
|
|
80
|
+
* session lock to hold reload/quit forever. Rejection is consumed here; the
|
|
81
|
+
* caller reports it when it wins the race, and a late rejection stays handled.
|
|
82
|
+
*/
|
|
83
|
+
async function boundedShutdownCleanup(
|
|
84
|
+
cleanup: Promise<void>,
|
|
85
|
+
timeoutMs = shutdownDrainTimeoutMs,
|
|
86
|
+
): Promise<"settled" | "timed-out" | "failed"> {
|
|
87
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
88
|
+
const outcome = cleanup.then(
|
|
89
|
+
() => "settled" as const,
|
|
90
|
+
() => "failed" as const,
|
|
91
|
+
);
|
|
92
|
+
const timeout = new Promise<"timed-out">((resolve) => {
|
|
93
|
+
timer = setTimeout(() => resolve("timed-out"), timeoutMs);
|
|
94
|
+
});
|
|
95
|
+
const result = await Promise.race([outcome, timeout]);
|
|
96
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @internal Test-only timeout override. */
|
|
101
|
+
export function _setShutdownDrainTimeoutForTesting(
|
|
102
|
+
timeoutMs: number | undefined,
|
|
103
|
+
): void {
|
|
104
|
+
shutdownDrainTimeoutMs = timeoutMs ?? DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS;
|
|
105
|
+
}
|
|
106
|
+
|
|
35
107
|
/** Register the delegate tool and clean up its parent-session resources. */
|
|
36
108
|
export default function delegateExtension(pi: ExtensionAPI): void {
|
|
109
|
+
// A /reload can reuse this module instance after the previous runtime closed
|
|
110
|
+
// its SQLite handle. Permit the new runtime to open a fresh backend; stale
|
|
111
|
+
// workers from the old runtime remain blocked from reopening it.
|
|
112
|
+
prepareTelemetryForSession();
|
|
113
|
+
|
|
37
114
|
pi.registerTool({
|
|
38
115
|
name: "delegate",
|
|
39
116
|
label: "Delegate to Subagents",
|
|
@@ -45,23 +122,70 @@ export default function delegateExtension(pi: ExtensionAPI): void {
|
|
|
45
122
|
prepareArguments: normalizeDelegateArguments,
|
|
46
123
|
|
|
47
124
|
async execute(_id, params: DelegateArguments, signal, onUpdate, ctx) {
|
|
125
|
+
const parentModelId = ctx.model?.id;
|
|
126
|
+
const tasks = params.tasks ?? [];
|
|
127
|
+
const parentSessionFile = (
|
|
128
|
+
ctx as { sessionManager?: { getSessionFile?(): string | undefined } }
|
|
129
|
+
).sessionManager?.getSessionFile?.();
|
|
130
|
+
|
|
131
|
+
let mode: string;
|
|
132
|
+
if (params.ticketAction) {
|
|
133
|
+
mode = params.ticketAction;
|
|
134
|
+
} else if (tasks.length === 0) {
|
|
135
|
+
mode = "manual";
|
|
136
|
+
} else if (params.async) {
|
|
137
|
+
mode = "async";
|
|
138
|
+
} else {
|
|
139
|
+
mode = "sync";
|
|
140
|
+
}
|
|
141
|
+
const taskCount = params.ticketAction ? 0 : tasks.length;
|
|
142
|
+
const callSpan = beginCall({
|
|
143
|
+
parentModel: parentModelId,
|
|
144
|
+
mode,
|
|
145
|
+
taskCount,
|
|
146
|
+
parentSessionFile,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
function failCall(): void {
|
|
150
|
+
callSpan.finish({
|
|
151
|
+
status: "failed",
|
|
152
|
+
totalTokens: 0,
|
|
153
|
+
totalCost: 0,
|
|
154
|
+
wallMs: Date.now() - callSpan.startedAt,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function succeedCall(): void {
|
|
159
|
+
callSpan.finish({
|
|
160
|
+
status: "success",
|
|
161
|
+
totalTokens: 0,
|
|
162
|
+
totalCost: 0,
|
|
163
|
+
wallMs: Date.now() - callSpan.startedAt,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
48
167
|
// Guard against pi dropping/renaming a symbol this extension imports
|
|
49
168
|
// before any operation-specific validation or early return.
|
|
50
169
|
const compatError = hostCompatError();
|
|
51
|
-
if (compatError)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
170
|
+
if (compatError) {
|
|
171
|
+
failCall();
|
|
172
|
+
return compatError;
|
|
173
|
+
}
|
|
55
174
|
|
|
56
175
|
const operationResult = validateDelegateOperationResult(
|
|
57
176
|
params,
|
|
58
177
|
parentModelId,
|
|
59
178
|
);
|
|
60
|
-
if (operationResult)
|
|
179
|
+
if (operationResult) {
|
|
180
|
+
failCall();
|
|
181
|
+
return operationResult;
|
|
182
|
+
}
|
|
61
183
|
|
|
62
184
|
// ── Poll action ───────────────────────────────────────────────────
|
|
63
185
|
if (params.ticketAction === "poll") {
|
|
64
|
-
|
|
186
|
+
const result = handlePoll(params, ctx);
|
|
187
|
+
succeedCall();
|
|
188
|
+
return result;
|
|
65
189
|
}
|
|
66
190
|
|
|
67
191
|
// ── Cancel action ─────────────────────────────────────────────────
|
|
@@ -70,12 +194,20 @@ export default function delegateExtension(pi: ExtensionAPI): void {
|
|
|
70
194
|
// A forced cancel flips the ticket to "cancelling" — keep the
|
|
71
195
|
// footer status in step (deduped; the preview path is a no-op).
|
|
72
196
|
syncDelegateStatus(ctx);
|
|
197
|
+
succeedCall();
|
|
73
198
|
return result;
|
|
74
199
|
}
|
|
75
200
|
|
|
76
201
|
// ── Wait action ────────────────────────────────────────────────────
|
|
77
202
|
if (params.ticketAction === "wait") {
|
|
78
|
-
|
|
203
|
+
try {
|
|
204
|
+
const result = await handleWait(params, signal, onUpdate, ctx);
|
|
205
|
+
succeedCall();
|
|
206
|
+
return result;
|
|
207
|
+
} catch (err) {
|
|
208
|
+
failCall();
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
79
211
|
}
|
|
80
212
|
|
|
81
213
|
// Agent discovery is intentionally parent-cwd-scoped: agent profiles are a
|
|
@@ -87,6 +219,7 @@ export default function delegateExtension(pi: ExtensionAPI): void {
|
|
|
87
219
|
|
|
88
220
|
// ── Help mode ─────────────────────────────────────────────────
|
|
89
221
|
if (!tasks.length) {
|
|
222
|
+
succeedCall();
|
|
90
223
|
return {
|
|
91
224
|
content: [{ type: "text", text: getSubagentManualMarkdown(agents) }],
|
|
92
225
|
details: {
|
|
@@ -108,25 +241,38 @@ export default function delegateExtension(pi: ExtensionAPI): void {
|
|
|
108
241
|
// across dispatches: edits to auth/models/settings/context files must be
|
|
109
242
|
// visible without restarting Pi.
|
|
110
243
|
invalidateHostDepsCache();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
244
|
+
try {
|
|
245
|
+
return await dispatchDelegate({
|
|
246
|
+
pi,
|
|
247
|
+
params,
|
|
248
|
+
ctx,
|
|
249
|
+
agents,
|
|
250
|
+
parentModelId,
|
|
251
|
+
parentDefaults: {
|
|
252
|
+
thinking: pi.getThinkingLevel(),
|
|
253
|
+
tools: pi.getActiveTools(),
|
|
254
|
+
},
|
|
255
|
+
signal,
|
|
256
|
+
onUpdate,
|
|
257
|
+
callSpan,
|
|
258
|
+
});
|
|
259
|
+
} catch (err) {
|
|
260
|
+
failCall();
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
124
263
|
},
|
|
125
264
|
|
|
126
265
|
renderCall: renderDelegateCall,
|
|
127
266
|
renderResult: renderDelegateResult,
|
|
128
267
|
});
|
|
129
268
|
|
|
269
|
+
// A reload may rebuild the extension runtime without re-invoking this
|
|
270
|
+
// module's default export. Re-open telemetry only after the prior shutdown
|
|
271
|
+
// handler has drained old workers and closed its connection.
|
|
272
|
+
pi.on("session_start", () => {
|
|
273
|
+
prepareTelemetryForSession();
|
|
274
|
+
});
|
|
275
|
+
|
|
130
276
|
// ── Background-work visibility (see status.ts) ──────────────────────────
|
|
131
277
|
// The turn settling with live tickets is the "looks idle but isn't" moment:
|
|
132
278
|
// warn once per ticket. The footer status carries it from there.
|
|
@@ -155,52 +301,137 @@ export default function delegateExtension(pi: ExtensionAPI): void {
|
|
|
155
301
|
|
|
156
302
|
// ── Session shutdown: abort tickets and dispose live pooled sessions ──
|
|
157
303
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
304
|
+
const shutdownGeneration = getTelemetryGeneration();
|
|
305
|
+
const shutdownDeadline = Date.now() + shutdownDrainTimeoutMs;
|
|
306
|
+
const ticketCompletions: Promise<void>[] = [];
|
|
307
|
+
let drainAttempted = false;
|
|
308
|
+
let telemetrySealed = false;
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
// Quit and /reload kill background work with no cancellable hook, so
|
|
312
|
+
// leave a trace. For quit the TUI is already stopped — stderr lands in
|
|
313
|
+
// the scrollback. For reload the TUI survives — warn in place. Switch
|
|
314
|
+
// and fork already passed the confirm guard above.
|
|
315
|
+
const active = activeTicketSummary();
|
|
316
|
+
if (active.tickets.length) {
|
|
317
|
+
if (event.reason === "quit") {
|
|
318
|
+
console.error(
|
|
319
|
+
`[delegate] pi exited with ${describeActiveTickets(active)} — aborted.`,
|
|
320
|
+
);
|
|
321
|
+
} else if (event.reason === "reload") {
|
|
322
|
+
try {
|
|
323
|
+
ctx.ui.notify(
|
|
324
|
+
`[delegate] reload aborted ${describeActiveTickets(active)}`,
|
|
325
|
+
"warning",
|
|
326
|
+
);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error(
|
|
329
|
+
"[delegate] reload shutdown notification failed",
|
|
330
|
+
error,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const ticket of ticketRegistry.values()) {
|
|
337
|
+
if (ticket.status === "running" || ticket.status === "cancelling") {
|
|
338
|
+
cancelTicketForShutdown(ticket);
|
|
339
|
+
}
|
|
340
|
+
// Include already-cancelled tickets too: a repeated shutdown event can
|
|
341
|
+
// race the first handler while its workers are still unwinding.
|
|
342
|
+
if (ticket.completion) ticketCompletions.push(ticket.completion);
|
|
343
|
+
}
|
|
344
|
+
syncDelegateStatus(ctx);
|
|
345
|
+
// The runtime is invalidated right after this handler returns; aborted
|
|
346
|
+
// tickets keep unwinding asynchronously and must find no cached ctx (or
|
|
347
|
+
// captured pi) to touch. The cancelled completion path still writes one
|
|
348
|
+
// final aggregate after late task results arrive, but never delivers UI.
|
|
349
|
+
clearDelegateStatusContext();
|
|
350
|
+
// A replacement session starts on its own leaf; stale tracking would make
|
|
351
|
+
// every ticket look cross-leaf (or, worse, look same-leaf by accident).
|
|
352
|
+
resetLeafTracking();
|
|
353
|
+
// Do NOT clear the ticket registry here — completed tickets are retained
|
|
354
|
+
// until their TTL cleanup. Pooled AgentSessions, however, own listeners
|
|
355
|
+
// and must be disposed before the parent session exits.
|
|
356
|
+
//
|
|
357
|
+
// Start pooled cleanup before waiting for ticket completion. Its immediate
|
|
358
|
+
// abort requests can help a worker blocked on a pooled session unwind;
|
|
359
|
+
// SQLite still stays open until both cleanup paths finish.
|
|
360
|
+
let poolCleanup: Promise<void>;
|
|
361
|
+
try {
|
|
362
|
+
poolCleanup = closeAllPooledAgents();
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error("[delegate] pooled-session shutdown start failed", error);
|
|
365
|
+
poolCleanup = Promise.resolve();
|
|
366
|
+
}
|
|
367
|
+
// Drain cooperatively, but never let a provider/tool hold reload or
|
|
368
|
+
// quit forever. A timed-out old runtime is sealed before a new one can
|
|
369
|
+
// reopen telemetry; its eventual task completion remains harmless.
|
|
370
|
+
drainAttempted = true;
|
|
371
|
+
const drain = await drainAsyncTickets(
|
|
372
|
+
ticketCompletions,
|
|
373
|
+
Math.max(0, shutdownDeadline - Date.now()),
|
|
374
|
+
);
|
|
375
|
+
if (!drain.drained) {
|
|
165
376
|
console.error(
|
|
166
|
-
`[delegate]
|
|
377
|
+
`[delegate] async-ticket shutdown drain exceeded ${shutdownDrainTimeoutMs}ms; continuing without late results`,
|
|
167
378
|
);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
379
|
+
// Seal immediately, before waiting on pooled-session cleanup. A worker
|
|
380
|
+
// that finishes during that second phase belongs to the old runtime
|
|
381
|
+
// and must not write into a freshly reopened backend.
|
|
382
|
+
telemetrySealed = sealTelemetryWrites(shutdownGeneration);
|
|
383
|
+
} else if (drain.failures.length) {
|
|
384
|
+
console.error(
|
|
385
|
+
"[delegate] async-ticket cleanup failed during shutdown:",
|
|
386
|
+
drain.failures,
|
|
172
387
|
);
|
|
173
388
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// captured pi) to touch. See the "cancelled"-at-entry guard in dispatch.
|
|
182
|
-
clearDelegateStatusContext();
|
|
183
|
-
// A replacement session starts on its own leaf; stale tracking would make
|
|
184
|
-
// every ticket look cross-leaf (or, worse, look same-leaf by accident).
|
|
185
|
-
resetLeafTracking();
|
|
186
|
-
// Do NOT clear the ticket registry here — completed tickets are retained
|
|
187
|
-
// until their TTL cleanup. Pooled AgentSessions, however, own listeners
|
|
188
|
-
// and must be disposed before the parent session exits.
|
|
189
|
-
//
|
|
190
|
-
// closeAllPooledAgents attempts every session (Promise.allSettled) before
|
|
191
|
-
// aggregating failures into an AggregateError, so swallowing here does not
|
|
192
|
-
// abandon remaining cleanup. Catch and log so a wedged session's failure
|
|
193
|
-
// stays observable instead of becoming an unhandled rejection — pi.on is
|
|
194
|
-
// EventEmitter-style and does not surface handler rejections — and so
|
|
195
|
-
// shutdown completes even when one pooled session failed to abort/dispose.
|
|
196
|
-
try {
|
|
197
|
-
await closeAllPooledAgents();
|
|
198
|
-
} catch (error) {
|
|
199
|
-
const failures = error instanceof AggregateError ? error.errors : [error];
|
|
200
|
-
console.error(
|
|
201
|
-
"[delegate] pooled-session cleanup failed during shutdown:",
|
|
202
|
-
failures,
|
|
389
|
+
|
|
390
|
+
// closeAllPooledAgents attempts every session (Promise.allSettled) before
|
|
391
|
+
// aggregating failures into an AggregateError, so swallowing here does
|
|
392
|
+
// not abandon remaining cleanup.
|
|
393
|
+
const poolResult = await boundedShutdownCleanup(
|
|
394
|
+
poolCleanup,
|
|
395
|
+
Math.max(0, shutdownDeadline - Date.now()),
|
|
203
396
|
);
|
|
397
|
+
if (poolResult === "timed-out") {
|
|
398
|
+
console.error(
|
|
399
|
+
`[delegate] pooled-session shutdown cleanup exceeded ${shutdownDrainTimeoutMs}ms; continuing`,
|
|
400
|
+
);
|
|
401
|
+
} else if (poolResult === "failed") {
|
|
402
|
+
// closeAllPooledAgents aggregates every session failure before
|
|
403
|
+
// rejecting, so the rejection is already a complete diagnostic.
|
|
404
|
+
try {
|
|
405
|
+
await poolCleanup;
|
|
406
|
+
} catch (error) {
|
|
407
|
+
const failures =
|
|
408
|
+
error instanceof AggregateError ? error.errors : [error];
|
|
409
|
+
console.error(
|
|
410
|
+
"[delegate] pooled-session cleanup failed during shutdown:",
|
|
411
|
+
failures,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
// pi.on is EventEmitter-style and does not surface handler rejections.
|
|
417
|
+
// Keep shutdown moving, but leave the failure visible.
|
|
418
|
+
console.error("[delegate] shutdown cleanup failed", error);
|
|
419
|
+
} finally {
|
|
420
|
+
if (!drainAttempted) {
|
|
421
|
+
// An unexpected earlier cleanup failure must not leave a worker with an
|
|
422
|
+
// unhandled rejection, but this fallback is bounded too.
|
|
423
|
+
await drainAsyncTickets(
|
|
424
|
+
ticketCompletions,
|
|
425
|
+
Math.max(0, shutdownDeadline - Date.now()),
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
// Once the bounded drain expires, old workers may still unwind. Seal
|
|
429
|
+
// their generation before closing SQLite so a later runtime cannot
|
|
430
|
+
// receive stale task/call writes.
|
|
431
|
+
if (!telemetrySealed) {
|
|
432
|
+
telemetrySealed = sealTelemetryWrites(shutdownGeneration);
|
|
433
|
+
}
|
|
434
|
+
closeTelemetry(shutdownGeneration);
|
|
204
435
|
}
|
|
205
436
|
});
|
|
206
437
|
}
|
package/format.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
TaskProgress,
|
|
8
8
|
TaskResult,
|
|
9
9
|
ToolActivity,
|
|
10
|
+
WorkspaceMode,
|
|
10
11
|
} from "./types.ts";
|
|
11
12
|
|
|
12
13
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -517,7 +518,10 @@ export function relativeTouchedSummary(
|
|
|
517
518
|
* in the same repository do not fabricate false conflicts from shared
|
|
518
519
|
* repository-wide git snapshots. */
|
|
519
520
|
export function findTouchedOverlaps(
|
|
520
|
-
results: readonly {
|
|
521
|
+
results: readonly {
|
|
522
|
+
attributedFiles?: string[];
|
|
523
|
+
workspace?: WorkspaceMode;
|
|
524
|
+
}[],
|
|
521
525
|
): string[] {
|
|
522
526
|
const counts = new Map<string, number>();
|
|
523
527
|
for (const r of results) {
|
package/host-compat.ts
CHANGED
|
@@ -23,6 +23,7 @@ const REQUIRED_EXPORTS: ExportCheck[] = [
|
|
|
23
23
|
{ name: "SettingsManager", requiredMember: "create" },
|
|
24
24
|
{ name: "SessionManager", requiredMember: "create" },
|
|
25
25
|
{ name: "SessionManager", requiredMember: "open" },
|
|
26
|
+
{ name: "SessionManager", requiredMember: "inMemory" },
|
|
26
27
|
{ name: "DefaultResourceLoader" },
|
|
27
28
|
{ name: "DefaultPackageManager" },
|
|
28
29
|
{ name: "createAgentSession" },
|