pi-extended-teams 2.1.16

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.
Files changed (130) hide show
  1. package/README.md +105 -0
  2. package/assets/pi-extended-teams-agent-navigation.png +0 -0
  3. package/assets/pi-extended-teams-in-action.png +0 -0
  4. package/extensions/agents/read-agent-report.ts +181 -0
  5. package/extensions/agents/read-agent-session-lifecycle.test.ts +605 -0
  6. package/extensions/agents/read-agent-session-lifecycle.ts +676 -0
  7. package/extensions/agents/read-agent.test.ts +3077 -0
  8. package/extensions/agents/read-agent.ts +1119 -0
  9. package/extensions/agents/write-agent.test.ts +513 -0
  10. package/extensions/agents/write-agent.ts +392 -0
  11. package/extensions/events/register-events.test.ts +465 -0
  12. package/extensions/events/register-events.ts +409 -0
  13. package/extensions/index.test.ts +1659 -0
  14. package/extensions/index.ts +1228 -0
  15. package/extensions/internal/agent-session-files.test.ts +164 -0
  16. package/extensions/internal/agent-session-files.ts +320 -0
  17. package/extensions/internal/debug.ts +44 -0
  18. package/extensions/internal/model-selection.ts +82 -0
  19. package/extensions/internal/pi-command.test.ts +191 -0
  20. package/extensions/internal/pi-command.ts +224 -0
  21. package/extensions/internal/pi-runtime-api.test.ts +43 -0
  22. package/extensions/internal/pi-runtime-api.ts +78 -0
  23. package/extensions/internal/schema.ts +19 -0
  24. package/extensions/internal/session-context-reference.test.ts +290 -0
  25. package/extensions/internal/session-context-reference.ts +430 -0
  26. package/extensions/internal/session-files.test.ts +225 -0
  27. package/extensions/internal/session-files.ts +273 -0
  28. package/extensions/internal/session-usage.ts +59 -0
  29. package/extensions/resources/spawn-resource-plan.test.ts +233 -0
  30. package/extensions/resources/spawn-resource-plan.ts +247 -0
  31. package/extensions/runtime/active-agent-sleep.test.ts +157 -0
  32. package/extensions/runtime/active-agent-sleep.ts +117 -0
  33. package/extensions/runtime/nested-read-agents.ts +22 -0
  34. package/extensions/runtime/pending-child-controller.test.ts +169 -0
  35. package/extensions/runtime/pending-child-controller.ts +280 -0
  36. package/extensions/runtime/types.ts +68 -0
  37. package/extensions/team/contracts.test.ts +111 -0
  38. package/extensions/team/lifecycle.test.ts +1095 -0
  39. package/extensions/team/lifecycle.ts +502 -0
  40. package/extensions/team/recipient-closure.test.ts +97 -0
  41. package/extensions/team/recipient-closure.ts +120 -0
  42. package/extensions/team/roster.test.ts +129 -0
  43. package/extensions/team/roster.ts +165 -0
  44. package/extensions/team/team-contracts.json +43 -0
  45. package/extensions/team/writer-screens.test.ts +50 -0
  46. package/extensions/team/writer-screens.ts +156 -0
  47. package/extensions/tools/agent-communication-tools.test.ts +306 -0
  48. package/extensions/tools/agent-communication-tools.ts +183 -0
  49. package/extensions/tools/coordination-tools.test.ts +670 -0
  50. package/extensions/tools/coordination-tools.ts +304 -0
  51. package/extensions/tools/delegation-guard.test.ts +95 -0
  52. package/extensions/tools/delegation-guard.ts +65 -0
  53. package/extensions/tools/file-claim-tools.test.ts +104 -0
  54. package/extensions/tools/file-claim-tools.ts +68 -0
  55. package/extensions/tools/model-tools.ts +53 -0
  56. package/extensions/tools/predefined-tools.test.ts +647 -0
  57. package/extensions/tools/predefined-tools.ts +331 -0
  58. package/extensions/tools/read-helper.test.ts +35 -0
  59. package/extensions/tools/task-runtime-tools.test.ts +334 -0
  60. package/extensions/tools/task-runtime-tools.ts +227 -0
  61. package/extensions/tools/team-tools.read-agent.test.ts +1924 -0
  62. package/extensions/tools/team-tools.ts +1270 -0
  63. package/extensions/ui/agent-follow-view.test.ts +520 -0
  64. package/extensions/ui/agent-follow-view.ts +779 -0
  65. package/extensions/ui/agent-navigation.test.ts +52 -0
  66. package/extensions/ui/agent-navigation.ts +55 -0
  67. package/extensions/ui/ansi.ts +16 -0
  68. package/extensions/ui/extensions-command.test.ts +336 -0
  69. package/extensions/ui/extensions-command.ts +282 -0
  70. package/extensions/ui/favorite-models-command.test.ts +280 -0
  71. package/extensions/ui/favorite-models-command.ts +423 -0
  72. package/extensions/ui/frame.ts +77 -0
  73. package/extensions/ui/input.ts +17 -0
  74. package/extensions/ui/read-agent-status.test.ts +117 -0
  75. package/extensions/ui/read-agent-status.ts +125 -0
  76. package/extensions/ui/renderers.ts +243 -0
  77. package/extensions/ui/status-widget.test.ts +143 -0
  78. package/extensions/ui/status-widget.ts +440 -0
  79. package/extensions/ui-frame.test.ts +69 -0
  80. package/package.json +75 -0
  81. package/skills/teams.md +256 -0
  82. package/src/adapters/terminal-registry.ts +78 -0
  83. package/src/adapters/tmux-adapter.test.ts +276 -0
  84. package/src/adapters/tmux-adapter.ts +200 -0
  85. package/src/orchestration/index.ts +437 -0
  86. package/src/orchestration/orchestrator.test.ts +396 -0
  87. package/src/orchestration/types.ts +125 -0
  88. package/src/utils/atomic-json.ts +21 -0
  89. package/src/utils/claims.test.ts +137 -0
  90. package/src/utils/claims.ts +169 -0
  91. package/src/utils/hooks.test.ts +75 -0
  92. package/src/utils/hooks.ts +35 -0
  93. package/src/utils/lifecycle-tombstone.test.ts +105 -0
  94. package/src/utils/lifecycle-tombstone.ts +268 -0
  95. package/src/utils/lock.race.child.ts +44 -0
  96. package/src/utils/lock.race.test.ts +198 -0
  97. package/src/utils/lock.test.ts +90 -0
  98. package/src/utils/lock.ts +186 -0
  99. package/src/utils/messaging.test.ts +337 -0
  100. package/src/utils/messaging.ts +441 -0
  101. package/src/utils/model-resolution.test.ts +231 -0
  102. package/src/utils/model-resolution.ts +322 -0
  103. package/src/utils/models.test.ts +8 -0
  104. package/src/utils/models.ts +115 -0
  105. package/src/utils/paths.ts +81 -0
  106. package/src/utils/predefined-teams/types.ts +48 -0
  107. package/src/utils/predefined-teams.save-template.test.ts +74 -0
  108. package/src/utils/predefined-teams.test.ts +441 -0
  109. package/src/utils/predefined-teams.ts +471 -0
  110. package/src/utils/read-helper-queue.ts +99 -0
  111. package/src/utils/report-events.test.ts +154 -0
  112. package/src/utils/report-events.ts +222 -0
  113. package/src/utils/runtime.test.ts +314 -0
  114. package/src/utils/runtime.ts +261 -0
  115. package/src/utils/security.test.ts +43 -0
  116. package/src/utils/settings.test.ts +480 -0
  117. package/src/utils/settings.ts +645 -0
  118. package/src/utils/shared-memory.test.ts +80 -0
  119. package/src/utils/shared-memory.ts +81 -0
  120. package/src/utils/tasks.race.test.ts +44 -0
  121. package/src/utils/tasks.test.ts +229 -0
  122. package/src/utils/tasks.ts +396 -0
  123. package/src/utils/teams.ts +231 -0
  124. package/src/utils/terminal-adapter.ts +103 -0
  125. package/src/utils/thinking-levels.test.ts +56 -0
  126. package/src/utils/thinking-levels.ts +47 -0
  127. package/src/utils/workflow-metadata.test.ts +11 -0
  128. package/src/utils/workflow-metadata.ts +88 -0
  129. package/src/utils/write-queue.test.ts +251 -0
  130. package/src/utils/write-queue.ts +202 -0
@@ -0,0 +1,676 @@
1
+ import type { AgentSession, SessionShutdownEvent } from "@mariozechner/pi-coding-agent";
2
+
3
+ export const NESTED_SESSION_TEARDOWN_TIMEOUT_MS = 2500;
4
+
5
+ export type NestedSessionShutdownReason = SessionShutdownEvent["reason"];
6
+ export type NestedSessionTeardownStatus = "settled" | "timed_out";
7
+ export type ReadAgentTeardownStatus = NestedSessionTeardownStatus | "persistence_failed" | "cleanup_failed";
8
+ export type ReadAgentStartupState = "pending" | "session_created" | "failed";
9
+
10
+ export interface ReadAgentDeliveryCloseResult {
11
+ cancelledDeliveries: number;
12
+ rawDeliverySettlement: Promise<void>;
13
+ }
14
+
15
+ export interface ReadAgentDeliveryState {
16
+ acceptingMessages?: boolean;
17
+ messageDeliveryClosed?: boolean;
18
+ messageDeliveryTail?: Promise<void>;
19
+ messageDeliveryGeneration?: number;
20
+ messageDeliveryCancellation?: Promise<void>;
21
+ cancelMessageDelivery?: () => void;
22
+ pendingMessageDeliveries?: number;
23
+ cancelledMessageDeliveries?: number;
24
+ }
25
+
26
+ export interface ManagedReadAgentLifecycleState extends ReadAgentDeliveryState {
27
+ session?: AgentSession;
28
+ startupState?: ReadAgentStartupState;
29
+ sessionCreation?: Promise<AgentSession | undefined>;
30
+ stopRequested?: boolean;
31
+ heartbeatTimer?: NodeJS.Timeout;
32
+ recipientClosurePromise?: Promise<void>;
33
+ persistedRecipientClosed?: boolean;
34
+ teardownState?: "active" | "stopping" | "quarantined" | "finalized" | "persistence_failed";
35
+ teardownPromise?: Promise<ReadAgentTeardownResult>;
36
+ teardownCleanupPromise?: Promise<void>;
37
+ teardownFinalizationPromise?: Promise<ReadAgentTeardownResult>;
38
+ teardownResult?: ReadAgentTeardownResult;
39
+ teardownError?: unknown;
40
+ resolveFinished?: () => void;
41
+ }
42
+
43
+ export interface NestedSessionTeardownResult {
44
+ status: NestedSessionTeardownStatus;
45
+ reason: NestedSessionShutdownReason;
46
+ extensionShutdown: "emitted" | "no_handlers" | "failed";
47
+ abort: "settled" | "rejected" | "unavailable" | "timed_out";
48
+ delivery: "settled" | "rejected" | "timed_out";
49
+ dispose: "settled" | "failed" | "deferred";
50
+ }
51
+
52
+ export interface ReadAgentFinalizationResult {
53
+ finalized: boolean;
54
+ removedMember: boolean;
55
+ releasedClaims: string[];
56
+ error?: string;
57
+ }
58
+
59
+ export interface ReadAgentTeardownResult extends Omit<NestedSessionTeardownResult, "status">, ReadAgentFinalizationResult {
60
+ status: ReadAgentTeardownStatus;
61
+ cancelledDeliveries: number;
62
+ persistenceClosed: boolean;
63
+ }
64
+
65
+ export interface ReadAgentTeardownOptions {
66
+ reason?: unknown;
67
+ closePersistence(): Promise<void>;
68
+ finalize(): Promise<ReadAgentFinalizationResult | void>;
69
+ onBoundedResult?(result: ReadAgentTeardownResult): Promise<void> | void;
70
+ }
71
+
72
+ interface DeliveryOutcome {
73
+ status: "delivered" | "cancelled";
74
+ }
75
+
76
+ interface NestedSessionLifecycle {
77
+ requestShutdown(
78
+ reason: unknown,
79
+ rawDeliverySettlement: Promise<void>,
80
+ timeoutMs?: number
81
+ ): Promise<NestedSessionTeardownResult>;
82
+ finalized: Promise<void>;
83
+ }
84
+
85
+ interface Deferred {
86
+ promise: Promise<void>;
87
+ resolve(): void;
88
+ reject(error: unknown): void;
89
+ }
90
+
91
+ const VALID_SHUTDOWN_REASONS = new Set<NestedSessionShutdownReason>([
92
+ "quit",
93
+ "reload",
94
+ "new",
95
+ "resume",
96
+ "fork",
97
+ ]);
98
+
99
+ const sessionLifecycles = new WeakMap<AgentSession, NestedSessionLifecycle>();
100
+
101
+ function createDeferred(): Deferred {
102
+ let resolve!: () => void;
103
+ let reject!: (error: unknown) => void;
104
+ const promise = new Promise<void>((done, fail) => {
105
+ resolve = done;
106
+ reject = fail;
107
+ });
108
+ return { promise, resolve, reject };
109
+ }
110
+
111
+ function normalizeShutdownReason(reason: unknown): NestedSessionShutdownReason {
112
+ return typeof reason === "string" && VALID_SHUTDOWN_REASONS.has(reason as NestedSessionShutdownReason)
113
+ ? reason as NestedSessionShutdownReason
114
+ : "quit";
115
+ }
116
+
117
+ function errorText(error: unknown): string {
118
+ return error instanceof Error ? error.message : String(error);
119
+ }
120
+
121
+ function normalizeFinalizationResult(result: ReadAgentFinalizationResult | void): ReadAgentFinalizationResult {
122
+ return result ?? {
123
+ finalized: true,
124
+ removedMember: false,
125
+ releasedClaims: [],
126
+ };
127
+ }
128
+
129
+ function ensureDeliveryCancellation(state: ReadAgentDeliveryState): Promise<void> {
130
+ if (state.messageDeliveryCancellation) return state.messageDeliveryCancellation;
131
+ const cancellation = createDeferred();
132
+ state.messageDeliveryCancellation = cancellation.promise;
133
+ state.cancelMessageDelivery = cancellation.resolve;
134
+ return cancellation.promise;
135
+ }
136
+
137
+ export class ReadAgentDeliveryCancelledError extends Error {
138
+ constructor(agentName: string) {
139
+ super(`Message delivery to ${agentName} was cancelled because the agent is finishing.`);
140
+ this.name = "ReadAgentDeliveryCancelledError";
141
+ }
142
+ }
143
+
144
+ export function closeReadAgentMessageDelivery(state: ReadAgentDeliveryState): ReadAgentDeliveryCloseResult {
145
+ const rawDeliverySettlement = state.messageDeliveryTail ?? Promise.resolve();
146
+ if (state.messageDeliveryClosed) {
147
+ return { cancelledDeliveries: 0, rawDeliverySettlement };
148
+ }
149
+
150
+ state.messageDeliveryClosed = true;
151
+ state.acceptingMessages = false;
152
+ state.messageDeliveryGeneration = (state.messageDeliveryGeneration ?? 0) + 1;
153
+ const cancelledDeliveries = state.pendingMessageDeliveries ?? 0;
154
+ state.cancelledMessageDeliveries = (state.cancelledMessageDeliveries ?? 0) + cancelledDeliveries;
155
+ state.cancelMessageDelivery?.();
156
+ return { cancelledDeliveries, rawDeliverySettlement };
157
+ }
158
+
159
+ export async function enqueueReadAgentMessageDelivery(
160
+ state: ReadAgentDeliveryState,
161
+ agentName: string,
162
+ send: () => Promise<void>
163
+ ): Promise<DeliveryOutcome> {
164
+ if (!state.acceptingMessages || state.messageDeliveryClosed) {
165
+ throw new Error(`Cannot send message to ${agentName}: agent is finishing.`);
166
+ }
167
+
168
+ const generation = state.messageDeliveryGeneration ?? 0;
169
+ state.messageDeliveryGeneration = generation;
170
+ const cancellation = ensureDeliveryCancellation(state);
171
+ state.pendingMessageDeliveries = (state.pendingMessageDeliveries ?? 0) + 1;
172
+
173
+ const previousDelivery = state.messageDeliveryTail ?? Promise.resolve();
174
+ const rawOutcome = previousDelivery.catch(() => {}).then(async (): Promise<DeliveryOutcome> => {
175
+ if (state.messageDeliveryClosed || state.messageDeliveryGeneration !== generation) {
176
+ return { status: "cancelled" };
177
+ }
178
+
179
+ let rawDelivery: Promise<void>;
180
+ try {
181
+ rawDelivery = Promise.resolve(send());
182
+ } catch (error) {
183
+ rawDelivery = Promise.reject(error);
184
+ }
185
+ // The sender-visible wait may be cancelled before the Pi promise settles. Keep a
186
+ // rejection observer on that raw promise so late provider failures are handled.
187
+ void rawDelivery.catch(() => {});
188
+ await rawDelivery;
189
+ return { status: "delivered" };
190
+ });
191
+ state.messageDeliveryTail = rawOutcome.then(() => {}, () => {});
192
+
193
+ try {
194
+ const outcome = await Promise.race([
195
+ rawOutcome,
196
+ cancellation.then((): DeliveryOutcome => ({ status: "cancelled" })),
197
+ ]);
198
+ if (outcome.status === "cancelled") throw new ReadAgentDeliveryCancelledError(agentName);
199
+ return outcome;
200
+ } finally {
201
+ state.pendingMessageDeliveries = Math.max(0, (state.pendingMessageDeliveries ?? 1) - 1);
202
+ }
203
+ }
204
+
205
+ async function operationTimedOut(operation: Promise<void>, timeoutMs: number): Promise<boolean> {
206
+ let settled = false;
207
+ const observed = operation.then(() => { settled = true; });
208
+ await Promise.resolve();
209
+ if (settled) return false;
210
+ if (timeoutMs <= 0) return true;
211
+
212
+ let timeout: NodeJS.Timeout | undefined;
213
+ const timedOut = await Promise.race([
214
+ observed.then(() => false),
215
+ new Promise<boolean>((resolve) => {
216
+ timeout = setTimeout(() => resolve(true), timeoutMs);
217
+ }),
218
+ ]);
219
+ if (timeout) clearTimeout(timeout);
220
+ return timedOut;
221
+ }
222
+
223
+ function installNestedSessionLifecycle(session: AgentSession): NestedSessionLifecycle {
224
+ const existing = sessionLifecycles.get(session);
225
+ if (existing) return existing;
226
+
227
+ const finalized = createDeferred();
228
+ let shutdownPromise: Promise<NestedSessionTeardownResult> | undefined;
229
+ let disposeOutcome: "settled" | "failed" = "settled";
230
+ let disposed = false;
231
+
232
+ const disposeOnce = (): void => {
233
+ if (disposed) return;
234
+ disposed = true;
235
+ try {
236
+ session.dispose();
237
+ finalized.resolve();
238
+ } catch (error) {
239
+ disposeOutcome = "failed";
240
+ finalized.reject(error);
241
+ }
242
+ };
243
+
244
+ const lifecycle: NestedSessionLifecycle = {
245
+ finalized: finalized.promise,
246
+ requestShutdown(reasonInput, rawDeliverySettlement, timeoutMs = NESTED_SESSION_TEARDOWN_TIMEOUT_MS) {
247
+ if (shutdownPromise) return shutdownPromise;
248
+ const reason = normalizeShutdownReason(reasonInput);
249
+
250
+ shutdownPromise = (async (): Promise<NestedSessionTeardownResult> => {
251
+ let extensionShutdown: NestedSessionTeardownResult["extensionShutdown"] = "no_handlers";
252
+ let rawExtensionShutdown = Promise.resolve();
253
+ try {
254
+ if (session.hasExtensionHandlers("session_shutdown")) {
255
+ extensionShutdown = "emitted";
256
+ rawExtensionShutdown = Promise.resolve(
257
+ session.extensionRunner.emit({ type: "session_shutdown", reason })
258
+ );
259
+ }
260
+ } catch {
261
+ extensionShutdown = "failed";
262
+ }
263
+ void rawExtensionShutdown.catch(() => {});
264
+
265
+ try {
266
+ session.clearQueue();
267
+ } catch {
268
+ // Queue clearing is defensive; abort still owns cancellation of active work.
269
+ }
270
+
271
+ let abortOutcome: NestedSessionTeardownResult["abort"] = "unavailable";
272
+ let rawAbort: Promise<void>;
273
+ if (typeof session.abort === "function") {
274
+ try {
275
+ rawAbort = Promise.resolve(session.abort());
276
+ } catch (error) {
277
+ rawAbort = Promise.reject(error);
278
+ }
279
+ void rawAbort.catch(() => {});
280
+ } else {
281
+ rawAbort = Promise.resolve();
282
+ }
283
+
284
+ let deliveryOutcome: NestedSessionTeardownResult["delivery"] = "settled";
285
+ let deliverySettled = false;
286
+ let abortSettled = typeof session.abort !== "function";
287
+ const observedExtensionShutdown = rawExtensionShutdown.then(
288
+ () => {},
289
+ () => { extensionShutdown = "failed"; }
290
+ );
291
+ const observedDelivery = Promise.resolve(rawDeliverySettlement).then(
292
+ () => { deliverySettled = true; },
293
+ () => {
294
+ deliverySettled = true;
295
+ deliveryOutcome = "rejected";
296
+ }
297
+ );
298
+ const observedAbort = rawAbort.then(
299
+ () => {
300
+ abortSettled = true;
301
+ abortOutcome = typeof session.abort === "function" ? "settled" : "unavailable";
302
+ },
303
+ () => {
304
+ abortSettled = true;
305
+ abortOutcome = "rejected";
306
+ }
307
+ );
308
+ const rawOperations = Promise.all([
309
+ observedExtensionShutdown,
310
+ observedDelivery,
311
+ observedAbort,
312
+ ]).then(() => {});
313
+ void rawOperations.then(disposeOnce);
314
+
315
+ const timedOut = await operationTimedOut(rawOperations, timeoutMs);
316
+ if (timedOut) {
317
+ if (!deliverySettled) deliveryOutcome = "timed_out";
318
+ if (!abortSettled) abortOutcome = "timed_out";
319
+ return {
320
+ status: "timed_out",
321
+ reason,
322
+ extensionShutdown,
323
+ abort: abortOutcome,
324
+ delivery: deliveryOutcome,
325
+ dispose: "deferred",
326
+ };
327
+ }
328
+
329
+ disposeOnce();
330
+ await finalized.promise.catch(() => {});
331
+ return {
332
+ status: "settled",
333
+ reason,
334
+ extensionShutdown,
335
+ abort: abortOutcome,
336
+ delivery: deliveryOutcome,
337
+ dispose: disposeOutcome,
338
+ };
339
+ })();
340
+ return shutdownPromise;
341
+ },
342
+ };
343
+
344
+ // A failed dispose is authoritative failure proof, not an unhandled promise.
345
+ // Teardown owners still await this observer to decide whether cleanup may run.
346
+ void lifecycle.finalized.catch(() => {});
347
+ sessionLifecycles.set(session, lifecycle);
348
+ return lifecycle;
349
+ }
350
+
351
+ export function installReadAgentSessionLifecycle(session: AgentSession): NestedSessionLifecycle {
352
+ return installNestedSessionLifecycle(session);
353
+ }
354
+
355
+ export function requestReadAgentTeardown(
356
+ state: ManagedReadAgentLifecycleState,
357
+ options: ReadAgentTeardownOptions
358
+ ): Promise<ReadAgentTeardownResult> {
359
+ state.stopRequested = true;
360
+ if (state.heartbeatTimer) clearInterval(state.heartbeatTimer);
361
+ state.heartbeatTimer = undefined;
362
+ const deliveryClose = closeReadAgentMessageDelivery(state);
363
+ // Request callers receive only the bounded public snapshot. The raw observer
364
+ // is explicit on teardownFinalizationPromise and is never returned implicitly.
365
+ if (state.teardownResult) return Promise.resolve(state.teardownResult);
366
+ if (state.teardownPromise) return state.teardownPromise;
367
+ state.teardownState = "stopping";
368
+
369
+ let finalizationResult: ReadAgentFinalizationResult | undefined;
370
+ const finalizeOnce = (): Promise<ReadAgentFinalizationResult> => {
371
+ if (!state.teardownCleanupPromise) {
372
+ state.teardownCleanupPromise = Promise.resolve()
373
+ .then(options.finalize)
374
+ .then((result) => {
375
+ finalizationResult = normalizeFinalizationResult(result);
376
+ if (finalizationResult.finalized && !finalizationResult.error) {
377
+ state.teardownState = "finalized";
378
+ state.resolveFinished?.();
379
+ } else {
380
+ state.teardownState = "quarantined";
381
+ if (finalizationResult.error) state.teardownError = new Error(finalizationResult.error);
382
+ }
383
+ })
384
+ .catch((error) => {
385
+ state.teardownError = error;
386
+ state.teardownState = "quarantined";
387
+ finalizationResult = {
388
+ finalized: false,
389
+ removedMember: false,
390
+ releasedClaims: [],
391
+ error: errorText(error),
392
+ };
393
+ });
394
+ }
395
+ return state.teardownCleanupPromise.then(() => finalizationResult ?? {
396
+ finalized: false,
397
+ removedMember: false,
398
+ releasedClaims: [],
399
+ error: "Lifecycle finalizer completed without a verified result.",
400
+ });
401
+ };
402
+
403
+ const unfinishedResult = (
404
+ status: "timed_out" | "persistence_failed",
405
+ reason: NestedSessionShutdownReason,
406
+ sessionResult: Omit<NestedSessionTeardownResult, "status" | "reason">,
407
+ error?: string
408
+ ): ReadAgentTeardownResult => ({
409
+ status,
410
+ reason,
411
+ ...sessionResult,
412
+ cancelledDeliveries: deliveryClose.cancelledDeliveries,
413
+ persistenceClosed: status !== "persistence_failed",
414
+ finalized: false,
415
+ removedMember: false,
416
+ releasedClaims: [],
417
+ ...(error ? { error } : {}),
418
+ });
419
+
420
+ const finalizedResult = (
421
+ sessionResult: NestedSessionTeardownResult,
422
+ finalization: ReadAgentFinalizationResult
423
+ ): ReadAgentTeardownResult => ({
424
+ ...sessionResult,
425
+ status: finalization.finalized && !finalization.error ? "settled" : "cleanup_failed",
426
+ cancelledDeliveries: deliveryClose.cancelledDeliveries,
427
+ persistenceClosed: true,
428
+ ...finalization,
429
+ });
430
+
431
+ const publishBoundedResult = (result: ReadAgentTeardownResult): ReadAgentTeardownResult => {
432
+ state.teardownResult = result;
433
+ void Promise.resolve(options.onBoundedResult?.(result)).catch(() => {});
434
+ return result;
435
+ };
436
+
437
+ const deferFinalization = (
438
+ rawOperations: Promise<void>,
439
+ timedOutResult: ReadAgentTeardownResult
440
+ ): void => {
441
+ state.teardownState = "quarantined";
442
+ state.teardownFinalizationPromise = rawOperations.then(async () => {
443
+ const finalization = await finalizeOnce();
444
+ const result: ReadAgentTeardownResult = {
445
+ ...timedOutResult,
446
+ status: finalization.finalized && !finalization.error ? "settled" : "cleanup_failed",
447
+ ...finalization,
448
+ };
449
+ state.teardownResult = result;
450
+ return result;
451
+ }).catch((error) => {
452
+ state.teardownError = error;
453
+ state.teardownState = "quarantined";
454
+ return publishBoundedResult({
455
+ ...timedOutResult,
456
+ status: "cleanup_failed",
457
+ finalized: false,
458
+ removedMember: false,
459
+ releasedClaims: [],
460
+ error: `Nested session disposal failed: ${errorText(error)}`,
461
+ });
462
+ });
463
+ };
464
+
465
+ const reason = normalizeShutdownReason(options.reason);
466
+ const continueAfterPersistence = async (deadline: number): Promise<ReadAgentTeardownResult> => {
467
+ let deliveryOutcome: NestedSessionTeardownResult["delivery"] = "settled";
468
+ let deliverySettled = false;
469
+ const observedDelivery = Promise.resolve(deliveryClose.rawDeliverySettlement).then(
470
+ () => { deliverySettled = true; },
471
+ () => {
472
+ deliverySettled = true;
473
+ deliveryOutcome = "rejected";
474
+ }
475
+ );
476
+
477
+ let session = state.session;
478
+ if (state.startupState === "pending" && state.sessionCreation) {
479
+ let startupSettled = false;
480
+ const observedStartup = Promise.resolve(state.sessionCreation).then(
481
+ (createdSession) => {
482
+ session = createdSession ?? state.session;
483
+ startupSettled = true;
484
+ },
485
+ (error) => {
486
+ state.teardownError = error;
487
+ session = state.session;
488
+ startupSettled = true;
489
+ }
490
+ );
491
+ const startupTimedOut = await operationTimedOut(
492
+ observedStartup,
493
+ Math.max(0, deadline - Date.now())
494
+ );
495
+
496
+ if (startupTimedOut && !startupSettled) {
497
+ const lateRawOperations = observedStartup.then(async () => {
498
+ if (session) {
499
+ const sessionLifecycle = installNestedSessionLifecycle(session);
500
+ await sessionLifecycle.requestShutdown(reason, deliveryClose.rawDeliverySettlement, 0);
501
+ await sessionLifecycle.finalized;
502
+ } else {
503
+ await observedDelivery;
504
+ }
505
+ });
506
+ if (!deliverySettled) deliveryOutcome = "timed_out";
507
+ const timedOutResult = unfinishedResult("timed_out", reason, {
508
+ extensionShutdown: "no_handlers",
509
+ abort: "timed_out",
510
+ delivery: deliveryOutcome,
511
+ dispose: "deferred",
512
+ });
513
+ deferFinalization(lateRawOperations, timedOutResult);
514
+ return publishBoundedResult(timedOutResult);
515
+ }
516
+ }
517
+
518
+ session ??= state.session;
519
+ if (!session) {
520
+ const deliveryTimedOut = await operationTimedOut(
521
+ observedDelivery,
522
+ Math.max(0, deadline - Date.now())
523
+ );
524
+ if (deliveryTimedOut) {
525
+ deliveryOutcome = "timed_out";
526
+ const timedOutResult = unfinishedResult("timed_out", reason, {
527
+ extensionShutdown: "no_handlers",
528
+ abort: "unavailable",
529
+ delivery: deliveryOutcome,
530
+ dispose: "deferred",
531
+ });
532
+ deferFinalization(observedDelivery, timedOutResult);
533
+ return publishBoundedResult(timedOutResult);
534
+ }
535
+
536
+ const finalization = await finalizeOnce();
537
+ return finalizedResult({
538
+ status: "settled",
539
+ reason,
540
+ extensionShutdown: "no_handlers",
541
+ abort: "unavailable",
542
+ delivery: deliveryOutcome,
543
+ dispose: "settled",
544
+ }, finalization);
545
+ }
546
+
547
+ const sessionLifecycle = installNestedSessionLifecycle(session);
548
+ const sessionResult = await sessionLifecycle.requestShutdown(
549
+ reason,
550
+ deliveryClose.rawDeliverySettlement,
551
+ Math.max(0, deadline - Date.now())
552
+ );
553
+ if (sessionResult.status === "timed_out") {
554
+ const timedOutResult = unfinishedResult("timed_out", sessionResult.reason, {
555
+ extensionShutdown: sessionResult.extensionShutdown,
556
+ abort: sessionResult.abort,
557
+ delivery: sessionResult.delivery,
558
+ dispose: sessionResult.dispose,
559
+ });
560
+ deferFinalization(sessionLifecycle.finalized, timedOutResult);
561
+ return publishBoundedResult(timedOutResult);
562
+ }
563
+
564
+ if (sessionResult.dispose === "failed") {
565
+ state.teardownState = "quarantined";
566
+ return publishBoundedResult({
567
+ ...sessionResult,
568
+ status: "cleanup_failed",
569
+ cancelledDeliveries: deliveryClose.cancelledDeliveries,
570
+ persistenceClosed: true,
571
+ finalized: false,
572
+ removedMember: false,
573
+ releasedClaims: [],
574
+ error: "Nested session disposal failed.",
575
+ });
576
+ }
577
+
578
+ const finalization = await finalizeOnce();
579
+ return finalizedResult(sessionResult, finalization);
580
+ };
581
+
582
+ state.teardownPromise = (async (): Promise<ReadAgentTeardownResult> => {
583
+ // Persistence closure and nested teardown share one budget. Closure proof is the
584
+ // hard gate: no nested or external cleanup may begin until this operation fulfills.
585
+ const deadline = Date.now() + NESTED_SESSION_TEARDOWN_TIMEOUT_MS;
586
+ let rawPersistenceClosure: Promise<void>;
587
+ try {
588
+ rawPersistenceClosure = Promise.resolve(options.closePersistence());
589
+ } catch (error) {
590
+ rawPersistenceClosure = Promise.reject(error);
591
+ }
592
+
593
+ let persistenceOutcome:
594
+ | { status: "closed" }
595
+ | { status: "rejected"; error: unknown }
596
+ | undefined;
597
+ const observedPersistenceClosure = rawPersistenceClosure.then(
598
+ () => {
599
+ state.persistedRecipientClosed = true;
600
+ persistenceOutcome = { status: "closed" };
601
+ },
602
+ (error) => {
603
+ persistenceOutcome = { status: "rejected", error };
604
+ }
605
+ );
606
+ const persistenceTimedOut = await operationTimedOut(
607
+ observedPersistenceClosure,
608
+ Math.max(0, deadline - Date.now())
609
+ );
610
+
611
+ if (persistenceTimedOut) {
612
+ const timeoutError = new Error(
613
+ `Persistence closure timed out after ${NESTED_SESSION_TEARDOWN_TIMEOUT_MS}ms.`
614
+ );
615
+ state.teardownError = timeoutError;
616
+ state.teardownState = "persistence_failed";
617
+ const timedOutResult = unfinishedResult("persistence_failed", reason, {
618
+ extensionShutdown: "no_handlers",
619
+ abort: "unavailable",
620
+ delivery: "settled",
621
+ dispose: "deferred",
622
+ }, timeoutError.message);
623
+
624
+ // Keep the raw operation observed. A late rejection remains non-destructive;
625
+ // a late fulfillment proves closure and is allowed exactly one continuation.
626
+ let persistenceContinuation!: Promise<ReadAgentTeardownResult>;
627
+ persistenceContinuation = observedPersistenceClosure.then(async () => {
628
+ if (persistenceOutcome?.status === "rejected") {
629
+ state.teardownError = persistenceOutcome.error;
630
+ state.teardownState = "persistence_failed";
631
+ const rejectedResult = unfinishedResult("persistence_failed", reason, {
632
+ extensionShutdown: "no_handlers",
633
+ abort: "unavailable",
634
+ delivery: "settled",
635
+ dispose: "deferred",
636
+ }, errorText(persistenceOutcome.error));
637
+ return publishBoundedResult(rejectedResult);
638
+ }
639
+
640
+ const continuationResult = await continueAfterPersistence(deadline);
641
+ const deferredContinuation = state.teardownFinalizationPromise;
642
+ if (
643
+ continuationResult.status === "timed_out"
644
+ && deferredContinuation
645
+ && deferredContinuation !== persistenceContinuation
646
+ ) {
647
+ return deferredContinuation;
648
+ }
649
+ state.teardownResult = continuationResult;
650
+ return continuationResult;
651
+ });
652
+ state.teardownFinalizationPromise = persistenceContinuation;
653
+ return publishBoundedResult(timedOutResult);
654
+ }
655
+
656
+ if (persistenceOutcome?.status === "rejected") {
657
+ state.teardownError = persistenceOutcome.error;
658
+ state.teardownState = "persistence_failed";
659
+ return publishBoundedResult(unfinishedResult("persistence_failed", reason, {
660
+ extensionShutdown: "no_handlers",
661
+ abort: "unavailable",
662
+ delivery: "settled",
663
+ dispose: "deferred",
664
+ }, errorText(persistenceOutcome.error)));
665
+ }
666
+
667
+ return continueAfterPersistence(deadline);
668
+ })();
669
+ void state.teardownPromise.then((result) => {
670
+ if (!state.teardownResult || state.teardownResult.status === "timed_out") {
671
+ state.teardownResult = result;
672
+ }
673
+ });
674
+
675
+ return state.teardownPromise;
676
+ }