@modelprofile.com/flexharness 10.23.0 → 10.24.1

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 (83) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.flexharness.admission.d.ts +17 -0
  3. package/dist_ts/classes.flexharness.admission.js +263 -0
  4. package/dist_ts/classes.flexharness.callbacks.d.ts +38 -0
  5. package/dist_ts/classes.flexharness.callbacks.js +313 -0
  6. package/dist_ts/classes.flexharness.compaction.d.ts +25 -0
  7. package/dist_ts/classes.flexharness.compaction.js +87 -0
  8. package/dist_ts/classes.flexharness.configurations.d.ts +11 -0
  9. package/dist_ts/classes.flexharness.configurations.js +137 -0
  10. package/dist_ts/classes.flexharness.core.d.ts +143 -0
  11. package/dist_ts/classes.flexharness.core.js +1538 -0
  12. package/dist_ts/classes.flexharness.d.ts +2 -437
  13. package/dist_ts/classes.flexharness.deletion.d.ts +31 -0
  14. package/dist_ts/classes.flexharness.deletion.js +396 -0
  15. package/dist_ts/classes.flexharness.imports.d.ts +24 -0
  16. package/dist_ts/classes.flexharness.imports.js +345 -0
  17. package/dist_ts/classes.flexharness.js +119 -9776
  18. package/dist_ts/classes.flexharness.lifecycle.d.ts +47 -0
  19. package/dist_ts/classes.flexharness.lifecycle.js +625 -0
  20. package/dist_ts/classes.flexharness.permissions.d.ts +19 -0
  21. package/dist_ts/classes.flexharness.permissions.js +450 -0
  22. package/dist_ts/classes.flexharness.projects.d.ts +55 -0
  23. package/dist_ts/classes.flexharness.projects.js +575 -0
  24. package/dist_ts/classes.flexharness.prompts.d.ts +44 -0
  25. package/dist_ts/classes.flexharness.prompts.js +659 -0
  26. package/dist_ts/classes.flexharness.providers.d.ts +18 -0
  27. package/dist_ts/classes.flexharness.providers.js +267 -0
  28. package/dist_ts/classes.flexharness.results.d.ts +26 -0
  29. package/dist_ts/classes.flexharness.results.js +89 -0
  30. package/dist_ts/classes.flexharness.reversions.d.ts +52 -0
  31. package/dist_ts/classes.flexharness.reversions.js +1306 -0
  32. package/dist_ts/classes.flexharness.runs.d.ts +63 -0
  33. package/dist_ts/classes.flexharness.runs.js +1058 -0
  34. package/dist_ts/classes.flexharness.sessions.d.ts +20 -0
  35. package/dist_ts/classes.flexharness.sessions.js +381 -0
  36. package/dist_ts/classes.flexharness.slashcommands.d.ts +40 -0
  37. package/dist_ts/classes.flexharness.slashcommands.js +458 -0
  38. package/dist_ts/classes.flexharness.subagents.d.ts +31 -0
  39. package/dist_ts/classes.flexharness.subagents.js +690 -0
  40. package/dist_ts/classes.flexharness.tooldispatch.d.ts +53 -0
  41. package/dist_ts/classes.flexharness.tooldispatch.js +418 -0
  42. package/dist_ts/interfaces.flexharness.d.ts +455 -0
  43. package/dist_ts/interfaces.flexharness.js +2 -0
  44. package/dist_ts/utils.flexharness.d.ts +19 -0
  45. package/dist_ts/utils.flexharness.js +83 -0
  46. package/dist_ts_compaction/plugins.d.ts +2 -2
  47. package/dist_ts_compaction/plugins.js +3 -3
  48. package/dist_ts_compaction/smartagent.compaction.js +6 -5
  49. package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +9 -0
  50. package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +13 -1
  51. package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +2 -2
  52. package/dist_ts_stores_nosqldb/classes.recordstore.js +3 -3
  53. package/package.json +3 -3
  54. package/readme.md +1 -1
  55. package/ts/00_commitinfo_data.ts +1 -1
  56. package/ts/classes.flexharness.admission.ts +331 -0
  57. package/ts/classes.flexharness.callbacks.ts +352 -0
  58. package/ts/classes.flexharness.compaction.ts +167 -0
  59. package/ts/classes.flexharness.configurations.ts +167 -0
  60. package/ts/classes.flexharness.core.ts +1830 -0
  61. package/ts/classes.flexharness.deletion.ts +616 -0
  62. package/ts/classes.flexharness.imports.ts +414 -0
  63. package/ts/classes.flexharness.lifecycle.ts +786 -0
  64. package/ts/classes.flexharness.permissions.ts +535 -0
  65. package/ts/classes.flexharness.projects.ts +1006 -0
  66. package/ts/classes.flexharness.prompts.ts +857 -0
  67. package/ts/classes.flexharness.providers.ts +322 -0
  68. package/ts/classes.flexharness.results.ts +98 -0
  69. package/ts/classes.flexharness.reversions.ts +1684 -0
  70. package/ts/classes.flexharness.runs.ts +1204 -0
  71. package/ts/classes.flexharness.sessions.ts +483 -0
  72. package/ts/classes.flexharness.slashcommands.ts +705 -0
  73. package/ts/classes.flexharness.subagents.ts +860 -0
  74. package/ts/classes.flexharness.tooldispatch.ts +475 -0
  75. package/ts/classes.flexharness.ts +147 -12567
  76. package/ts/interfaces.flexharness.ts +517 -0
  77. package/ts/readme.md +1 -1
  78. package/ts/utils.flexharness.ts +121 -0
  79. package/ts_compaction/plugins.ts +2 -2
  80. package/ts_compaction/smartagent.compaction.ts +5 -4
  81. package/ts_stores_nosqldb/classes.nosqldbstores.ts +13 -0
  82. package/ts_stores_nosqldb/classes.recordstore.ts +5 -5
  83. package/ts_stores_nosqldb/readme.md +7 -0
@@ -0,0 +1,786 @@
1
+ import { FlexHarnessAbortError } from './errors.js';
2
+ import type { IFlexAgentContextInvocation, IFlexExecutionContextHandle } from './interfaces.js';
3
+ import { validateIdentifier } from './utils.validation.js';
4
+ import type {
5
+ IStoredSessionState,
6
+ IOrphanedProviderRelease,
7
+ IOrphanedTombstoneCleanup,
8
+ IOrphanedTombstoneOwner,
9
+ IOrphanedExecutionContextOwner,
10
+ IStorageState,
11
+ IActiveRun,
12
+ } from './interfaces.flexharness.js';
13
+ import { scopeRetirementMessage, combineErrors } from './utils.flexharness.js';
14
+ import type { FlexHarnessCore } from './classes.flexharness.core.js';
15
+
16
+ /**
17
+ * Harness and scope lifecycle: namespace fences, storage drains, orphaned resources, scope
18
+ * retirement and disposal.
19
+ */
20
+ export class FlexHarnessLifecycle<TScope> {
21
+ private readonly scopeRetirements = new Map<string, Promise<void>>();
22
+ public readonly storageDrains = new Map<string, Promise<void>>();
23
+ public readonly storageCompactorLifecycleControllers = new Map<string, AbortController>();
24
+ private orphanedResourceQueue: Promise<void> = Promise.resolve();
25
+ public readonly orphanedStoredSessions = new Set<IStoredSessionState>();
26
+ public readonly orphanedExecutionContexts = new Map<
27
+ IFlexExecutionContextHandle,
28
+ IOrphanedExecutionContextOwner
29
+ >();
30
+ private readonly orphanedProviderReleases = new Map<string, IOrphanedProviderRelease>();
31
+ private readonly orphanedTombstoneCleanups = new Map<string, IOrphanedTombstoneCleanup>();
32
+ public readonly orphanedTombstoneOwners = new Map<string, IOrphanedTombstoneOwner<TScope>>();
33
+ private disposePromise?: Promise<void>;
34
+
35
+ constructor(private readonly core: FlexHarnessCore<TScope>) {}
36
+
37
+ public retireScope(scopeId: string): Promise<void> {
38
+ this.core.commands.assertNoAmbientSlashCommandTeardown({ scopeId });
39
+ this.core.assertOpen();
40
+ validateIdentifier(scopeId, 'scopeId');
41
+ const existing = this.scopeRetirements.get(scopeId);
42
+ if (existing) return existing;
43
+ const admission = this.core.getScopeAdmission(scopeId);
44
+ admission.generation++;
45
+ admission.retiring = true;
46
+ let retirement!: Promise<void>;
47
+ retirement = this.retireScopeInternal(scopeId).finally(() => {
48
+ if (this.scopeRetirements.get(scopeId) === retirement) this.scopeRetirements.delete(scopeId);
49
+ admission.retiring = false;
50
+ this.core.pruneScopeAdmission(scopeId, admission);
51
+ });
52
+ this.scopeRetirements.set(scopeId, retirement);
53
+ return retirement;
54
+ }
55
+
56
+ public async dispose(): Promise<void> {
57
+ this.core.commands.assertNoAmbientSlashCommandTeardown();
58
+ if (this.disposePromise) return this.disposePromise;
59
+ this.core.closed = true;
60
+ let disposal!: Promise<void>;
61
+ disposal = this.disposeInternal().catch((error) => {
62
+ if (this.disposePromise === disposal) this.disposePromise = undefined;
63
+ throw error;
64
+ });
65
+ this.disposePromise = disposal;
66
+ return disposal;
67
+ }
68
+
69
+ public async fenceNamespace(state: IStorageState, currentRun: IActiveRun, cause: unknown): Promise<void> {
70
+ if (state.lifecycle === 'retired') return;
71
+ if (this.storageDrains.has(state.storageKey)) {
72
+ state.fenceAdditionalErrors.push(cause);
73
+ return;
74
+ }
75
+ if (state.fenceInProgress) {
76
+ state.fenceAdditionalErrors.push(cause);
77
+ return;
78
+ }
79
+ state.fenceInProgress = true;
80
+ state.lifecycle = 'fenced';
81
+ const reason = this.core.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.'));
82
+ if (!state.compactorLifecycleController.signal.aborted) {
83
+ state.compactorLifecycleController.abort(reason);
84
+ }
85
+ const cleanupErrors: unknown[] = [];
86
+ const ambientContext = this.core.compaction.compactorInvocationContext.getStore();
87
+ const invocation = ambientContext?.storageKey === state.storageKey
88
+ && ambientContext.sessionId === currentRun.sessionId
89
+ ? ambientContext
90
+ : this.core.compaction.effectiveRunCompactorContext(currentRun);
91
+ const contextFor = (sessionId: string) => this.core.compaction.createCompactorContext(
92
+ invocation.scopeId,
93
+ invocation.scope,
94
+ state.storageKey,
95
+ sessionId,
96
+ );
97
+ try {
98
+ for (const stored of state.sessions.values()) {
99
+ this.core.compaction.abortCompactorLifecycle(stored, reason);
100
+ this.core.prompts.cancelStoredPromptQueue(
101
+ stored,
102
+ reason,
103
+ stored === currentRun.stored ? currentRun.queueId : undefined,
104
+ contextFor(stored.session.sessionId),
105
+ );
106
+ }
107
+ const otherRuns = [...state.activeRuns.values()].filter((run) => run !== currentRun);
108
+ for (const run of otherRuns) this.core.runs.cancelRun(run, reason, contextFor(run.sessionId));
109
+ const dependentRuns = otherRuns.filter((run) =>
110
+ this.core.sessionsAreDependencyRelated(state, currentRun.sessionId, run.sessionId));
111
+ const independentRuns = otherRuns.filter((run) => !dependentRuns.includes(run));
112
+ const runResults = await Promise.allSettled(independentRuns.map((run) => run.completion));
113
+ for (const result of runResults) {
114
+ if (result.status === 'rejected') this.core.appendUnexpectedErrors(cleanupErrors, result.reason);
115
+ }
116
+ if (
117
+ dependentRuns.length > 0
118
+ || (currentRun.delegatedRunAdmissionOwner !== undefined
119
+ && state.delegatedRunAdmissionOwners.has(currentRun.delegatedRunAdmissionOwner))
120
+ ) {
121
+ state.fenceAdditionalErrors.push(cause, ...cleanupErrors);
122
+ const stateLoad = this.core.stateLoads.get(state.storageKey);
123
+ if (!stateLoad) {
124
+ throw combineErrors([cause, ...cleanupErrors, new Error(
125
+ 'The fenced namespace no longer has durable cleanup ownership.',
126
+ )]);
127
+ }
128
+ const deferredDrain = this.drainStorage(
129
+ state.storageKey,
130
+ stateLoad,
131
+ reason,
132
+ { scopeId: invocation.scopeId, scope: invocation.scope },
133
+ );
134
+ void deferredDrain.catch(() => undefined);
135
+ return;
136
+ }
137
+ cleanupErrors.push(...await this.core.admission.settleDelegatedRunAdmissionOwners([
138
+ ...state.delegatedRunAdmissionOwners,
139
+ ]));
140
+ const detachedCleanupAttempts = this.core.providers.beginDetachedCleanupSettlement(state);
141
+ await Promise.allSettled([...state.sessionInitializations.values()]);
142
+ cleanupErrors.push(...await this.closeOrphanedResources(state.storageKey));
143
+ await state.scopeQueue;
144
+ await this.core.projects.awaitProjectManagementOperations(
145
+ state.storageKey,
146
+ [
147
+ ...state.sessions.keys(),
148
+ ...state.retainedSessionCleanups.keys(),
149
+ ],
150
+ );
151
+ const currentTombstoneRoot = state.tombstones.get(currentRun.sessionId)?.rootSessionId
152
+ ?? currentRun.sessionId;
153
+ const currentTombstoneCleanup = state.tombstoneCleanups.get(currentTombstoneRoot);
154
+ const currentTombstoneSessions = new Set(
155
+ this.core.deletion.tombstoneGroup(state, currentTombstoneRoot).map((tombstone) => tombstone.sessionId),
156
+ );
157
+ if (currentTombstoneCleanup) {
158
+ this.retainOrphanedTombstoneCleanup(
159
+ state.storageKey,
160
+ currentTombstoneRoot,
161
+ currentTombstoneCleanup,
162
+ );
163
+ }
164
+ const tombstoneAttempts = [...state.tombstoneCleanups.entries()]
165
+ .filter(([rootSessionId]) => rootSessionId !== currentTombstoneRoot);
166
+ const tombstoneResults = await Promise.allSettled(
167
+ tombstoneAttempts.map(([, cleanup]) => cleanup),
168
+ );
169
+ for (const result of tombstoneResults) {
170
+ if (result.status === 'rejected') this.core.appendUnexpectedErrors(cleanupErrors, result.reason);
171
+ }
172
+ const attemptedTombstones = new Set(tombstoneAttempts.map(([sessionId]) => sessionId));
173
+ const attemptedTombstoneSessions = new Set([...attemptedTombstones].flatMap((rootSessionId) =>
174
+ this.core.deletion.tombstoneGroup(state, rootSessionId).map((tombstone) => tombstone.sessionId)));
175
+ const storedSessions = new Set([
176
+ ...state.sessions.values(),
177
+ ...[...state.retainedSessionCleanups]
178
+ .filter(([sessionId]) =>
179
+ !currentTombstoneSessions.has(sessionId)
180
+ && !attemptedTombstoneSessions.has(sessionId))
181
+ .map(([, retained]) => retained.stored),
182
+ ]);
183
+ for (const stored of storedSessions) {
184
+ try {
185
+ await this.abortStoredSession(stored, reason, contextFor(stored.session.sessionId));
186
+ } catch (error) {
187
+ cleanupErrors.push(this.core.projectOperationError(
188
+ error,
189
+ 'agentSession',
190
+ currentRun.scopeId,
191
+ stored.session.sessionId,
192
+ currentRun.runId,
193
+ ));
194
+ }
195
+ try {
196
+ await this.closeStoredSession(stored, contextFor(stored.session.sessionId));
197
+ } catch (error) {
198
+ if (!this.storedSessionCleanupCompleted(stored)) {
199
+ cleanupErrors.push(this.core.projectOperationError(
200
+ error,
201
+ 'toolCleanup',
202
+ currentRun.scopeId,
203
+ stored.session.sessionId,
204
+ currentRun.runId,
205
+ ));
206
+ }
207
+ }
208
+ if (stored !== currentRun.stored) this.core.prompts.purgeStoredPromptQueue(stored);
209
+ }
210
+ cleanupErrors.push(...await this.core.providers.settleDetachedCleanups(state, detachedCleanupAttempts));
211
+ state.fenceAdditionalErrors.length = 0;
212
+ if (cleanupErrors.length > 0) throw combineErrors([cause, ...cleanupErrors]);
213
+ state.lifecycle = 'retired';
214
+ state.sessions.clear();
215
+ const currentRetainedCleanups = [...state.retainedSessionCleanups]
216
+ .filter(([sessionId]) => currentTombstoneSessions.has(sessionId));
217
+ state.retainedSessionCleanups.clear();
218
+ for (const [sessionId, retained] of currentRetainedCleanups) {
219
+ state.retainedSessionCleanups.set(sessionId, retained);
220
+ }
221
+ state.sessionDeletions.clear();
222
+ state.tombstoneCleanups.clear();
223
+ if (currentTombstoneCleanup) {
224
+ state.tombstoneCleanups.set(currentTombstoneRoot, currentTombstoneCleanup);
225
+ }
226
+ state.activeRuns.clear();
227
+ state.delegatedRunAdmissionOwners.clear();
228
+ state.pendingPermissions.clear();
229
+ state.initializingSessions.clear();
230
+ state.sessionInitializations.clear();
231
+ this.core.stateLoads.delete(state.storageKey);
232
+ if (
233
+ this.storageCompactorLifecycleControllers.get(state.storageKey)
234
+ === state.compactorLifecycleController
235
+ ) this.storageCompactorLifecycleControllers.delete(state.storageKey);
236
+ } finally {
237
+ state.fenceInProgress = false;
238
+ }
239
+ }
240
+
241
+ public deferNamespaceDrain(
242
+ state: IStorageState,
243
+ cause: unknown,
244
+ invocation?: { scopeId: string; scope: TScope },
245
+ ): void {
246
+ state.lifecycle = 'fenced';
247
+ state.fenceAdditionalErrors.push(cause);
248
+ const stateLoad = this.core.stateLoads.get(state.storageKey);
249
+ if (!stateLoad) return;
250
+ const drain = this.drainStorage(
251
+ state.storageKey,
252
+ stateLoad,
253
+ this.core.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.')),
254
+ invocation,
255
+ );
256
+ void drain.catch(() => undefined);
257
+ }
258
+
259
+ public deferReversionReleaseDrain(
260
+ state: IStorageState,
261
+ stored: IStoredSessionState,
262
+ scopeId: string,
263
+ scope: TScope,
264
+ ): void {
265
+ state.lifecycle = 'fenced';
266
+ const reason = this.core.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.'));
267
+ this.core.compaction.abortCompactorLifecycle(stored, reason);
268
+ const stateLoad = this.core.stateLoads.get(state.storageKey);
269
+ if (!stateLoad) return;
270
+ const drain = this.drainStorage(
271
+ state.storageKey,
272
+ stateLoad,
273
+ reason,
274
+ { scopeId, scope },
275
+ );
276
+ void drain.catch(() => undefined);
277
+ }
278
+
279
+ public async closeStoredSession(
280
+ stored: IStoredSessionState,
281
+ context: IFlexAgentContextInvocation<unknown> = stored.compactorContext!,
282
+ ): Promise<void> {
283
+ const errors: unknown[] = [];
284
+ if (!stored.agentSessionCloseCompleted) {
285
+ try {
286
+ await this.core.compaction.withCompactorContext(
287
+ context,
288
+ () => stored.agentSession.close(),
289
+ );
290
+ if (!stored.agentSession.closeCleanupCompleted) {
291
+ throw new Error('AgentSession.close() resolved before cleanup ownership was released.');
292
+ }
293
+ } catch (error) {
294
+ errors.push(error);
295
+ }
296
+ if (stored.agentSession.closeCleanupCompleted) {
297
+ stored.agentSessionCloseCompleted = true;
298
+ }
299
+ }
300
+ if (stored.agentSessionCloseCompleted && !stored.executionContextCloseCompleted) {
301
+ try {
302
+ await stored.executionContextHandle!.close!();
303
+ stored.executionContextCloseCompleted = true;
304
+ delete stored.executionContextHandle;
305
+ delete stored.jobs;
306
+ } catch (error) {
307
+ errors.push(error);
308
+ }
309
+ }
310
+ if (stored.agentSessionCloseCompleted) {
311
+ stored.agentSessionAbortCompleted = true;
312
+ }
313
+ if (stored.agentSessionCloseCompleted && !stored.agentEventStoreReleased) {
314
+ try {
315
+ await this.core.stores.agentEvents.releaseSession!(stored.storageKey, stored.session.sessionId);
316
+ stored.agentEventStoreReleased = true;
317
+ } catch (error) {
318
+ errors.push(error);
319
+ }
320
+ }
321
+ if (
322
+ stored.agentSessionCloseCompleted
323
+ && stored.executionContextCloseCompleted
324
+ && !stored.jobStoreReleased
325
+ ) {
326
+ try {
327
+ await this.core.stores.jobs.releaseSession!(stored.storageKey, stored.session.sessionId);
328
+ stored.jobStoreReleased = true;
329
+ } catch (error) {
330
+ errors.push(error);
331
+ }
332
+ }
333
+ if (stored.metricsFailure !== undefined) {
334
+ errors.push(stored.metricsFailure);
335
+ // Runtime ownership has settled; the fenced namespace will be reloaded from its durable WAL.
336
+ if (stored.agentSessionCloseCompleted) delete stored.metricsFailure;
337
+ }
338
+ if (errors.length > 0) throw combineErrors(errors);
339
+ }
340
+
341
+ public async abortStoredSession(
342
+ stored: IStoredSessionState,
343
+ reason: FlexHarnessAbortError,
344
+ context: IFlexAgentContextInvocation<unknown> = stored.compactorContext!,
345
+ ): Promise<void> {
346
+ if (stored.agentSessionAbortCompleted) return;
347
+ await this.core.compaction.withCompactorContext(
348
+ context,
349
+ () => stored.agentSession.abortSession(reason, { abortBackgroundJobs: true }),
350
+ );
351
+ stored.agentSessionAbortCompleted = true;
352
+ }
353
+
354
+ public storedSessionCleanupCompleted(stored: IStoredSessionState): boolean {
355
+ return stored.agentSessionCloseCompleted
356
+ && stored.executionContextCloseCompleted
357
+ && stored.agentEventStoreReleased
358
+ && stored.jobStoreReleased;
359
+ }
360
+
361
+ public async releasePartialProviderSession(
362
+ providerName: 'agentEvents' | 'jobs',
363
+ storageKey: string,
364
+ sessionId: string,
365
+ ): Promise<void> {
366
+ const retained = this.retainPartialProviderSession(providerName, storageKey, sessionId);
367
+ if (!retained) return;
368
+ await retained.release();
369
+ }
370
+
371
+ public retainPartialProviderSession(
372
+ providerName: 'agentEvents' | 'jobs',
373
+ storageKey: string,
374
+ sessionId: string,
375
+ ): IOrphanedProviderRelease | undefined {
376
+ const provider = this.core.stores[providerName];
377
+ if (!provider.releaseSession) return undefined;
378
+ const key = JSON.stringify([providerName, storageKey, sessionId]);
379
+ let retained = this.orphanedProviderReleases.get(key);
380
+ if (!retained) {
381
+ retained = {
382
+ providerName,
383
+ storageKey,
384
+ sessionId,
385
+ release: async () => {
386
+ await provider.releaseSession!(storageKey, sessionId);
387
+ if (this.orphanedProviderReleases.get(key) === retained) {
388
+ this.orphanedProviderReleases.delete(key);
389
+ }
390
+ },
391
+ };
392
+ this.orphanedProviderReleases.set(key, retained);
393
+ }
394
+ return retained;
395
+ }
396
+
397
+ public hasOrphanedSessionResources(storageKey: string, sessionId: string): boolean {
398
+ return [...this.orphanedStoredSessions].some((stored) =>
399
+ stored.storageKey === storageKey && stored.session.sessionId === sessionId)
400
+ || [...this.orphanedExecutionContexts.values()].some((owner) =>
401
+ owner.storageKey === storageKey && owner.sessionId === sessionId)
402
+ || [...this.orphanedProviderReleases.values()].some((retained) =>
403
+ retained.storageKey === storageKey && retained.sessionId === sessionId);
404
+ }
405
+
406
+ private retainOrphanedTombstoneCleanup(
407
+ storageKey: string,
408
+ sessionId: string,
409
+ completion: Promise<void>,
410
+ ): void {
411
+ const key = JSON.stringify([storageKey, sessionId]);
412
+ if (this.orphanedTombstoneCleanups.has(key)) return;
413
+ const retained: IOrphanedTombstoneCleanup = { storageKey, sessionId, completion };
414
+ this.orphanedTombstoneCleanups.set(key, retained);
415
+ void completion.then(
416
+ () => {
417
+ if (this.orphanedTombstoneCleanups.get(key) === retained) {
418
+ this.orphanedTombstoneCleanups.delete(key);
419
+ }
420
+ },
421
+ () => {
422
+ if (this.orphanedTombstoneCleanups.get(key) === retained) {
423
+ this.orphanedTombstoneCleanups.delete(key);
424
+ }
425
+ },
426
+ );
427
+ }
428
+
429
+ public closeOrphanedResources(storageKey?: string): Promise<unknown[]> {
430
+ const operation = this.orphanedResourceQueue.then(() =>
431
+ this.closeOrphanedResourcesInternal(storageKey));
432
+ this.orphanedResourceQueue = operation.then(() => undefined, () => undefined);
433
+ return operation;
434
+ }
435
+
436
+ private async closeOrphanedResourcesInternal(storageKey?: string): Promise<unknown[]> {
437
+ const errors: unknown[] = [];
438
+ for (const [key, owner] of [...this.orphanedTombstoneOwners]) {
439
+ if (storageKey !== undefined && owner.state.storageKey !== storageKey) continue;
440
+ try {
441
+ await this.core.deletion.finishTombstoneCleanup(
442
+ owner.state,
443
+ owner.rootSessionId,
444
+ owner.scopeId,
445
+ { scopeId: owner.scopeId, scope: owner.scope },
446
+ );
447
+ this.orphanedTombstoneOwners.delete(key);
448
+ } catch (error) {
449
+ errors.push(error);
450
+ }
451
+ }
452
+ const tombstoneCleanups = [...this.orphanedTombstoneCleanups.values()]
453
+ .filter((retained) => storageKey === undefined || retained.storageKey === storageKey);
454
+ const tombstoneResults = await Promise.allSettled(
455
+ tombstoneCleanups.map((retained) => retained.completion),
456
+ );
457
+ for (const result of tombstoneResults) {
458
+ if (result.status === 'rejected') this.core.appendUnexpectedErrors(errors, result.reason);
459
+ }
460
+ for (const stored of [...this.orphanedStoredSessions]) {
461
+ if (storageKey !== undefined && stored.storageKey !== storageKey) continue;
462
+ try {
463
+ await this.closeStoredSession(stored);
464
+ this.orphanedStoredSessions.delete(stored);
465
+ } catch (error) {
466
+ errors.push(error);
467
+ }
468
+ }
469
+ for (const [handle, owner] of [...this.orphanedExecutionContexts]) {
470
+ if (storageKey !== undefined && owner.storageKey !== storageKey) continue;
471
+ try {
472
+ await handle.close!();
473
+ this.orphanedExecutionContexts.delete(handle);
474
+ } catch (error) {
475
+ errors.push(error);
476
+ }
477
+ }
478
+ for (const [key, retained] of [...this.orphanedProviderReleases]) {
479
+ if (storageKey !== undefined && retained.storageKey !== storageKey) continue;
480
+ if (
481
+ retained.providerName === 'jobs'
482
+ && [...this.orphanedExecutionContexts.values()].some((owner) =>
483
+ owner.storageKey === retained.storageKey && owner.sessionId === retained.sessionId)
484
+ ) continue;
485
+ try {
486
+ await retained.release();
487
+ } catch (error) {
488
+ errors.push(error);
489
+ this.orphanedProviderReleases.set(key, retained);
490
+ }
491
+ }
492
+ return errors;
493
+ }
494
+
495
+ private orphanedResourceStorageKeys(): Set<string> {
496
+ return new Set([
497
+ ...[...this.orphanedStoredSessions].map((stored) => stored.storageKey),
498
+ ...[...this.orphanedExecutionContexts.values()].map((owner) => owner.storageKey),
499
+ ...[...this.orphanedProviderReleases.values()].map((retained) => retained.storageKey),
500
+ ...[...this.orphanedTombstoneCleanups.values()].map((retained) => retained.storageKey),
501
+ ...[...this.orphanedTombstoneOwners.values()].map((owner) => owner.state.storageKey),
502
+ ]);
503
+ }
504
+
505
+ private async retireScopeInternal(scopeId: string): Promise<void> {
506
+ const scope = await this.core.scopeResolver.resolveScope(scopeId);
507
+ this.core.assertOpen();
508
+ validateIdentifier(scope.storageKey, 'resolved storageKey');
509
+ this.core.commands.assertNoAmbientSlashCommandTeardown({ storageKey: scope.storageKey });
510
+ const errors: unknown[] = [];
511
+ const listingSettlement = this.core.commands.abortSlashCommandListings(
512
+ scope.storageKey,
513
+ this.core.trustInternalError(new FlexHarnessAbortError(scopeRetirementMessage)),
514
+ );
515
+ if (listingSettlement) await listingSettlement;
516
+ await this.core.commands.abortSlashCommandExecutions(
517
+ scope.storageKey,
518
+ this.core.trustInternalError(new FlexHarnessAbortError(scopeRetirementMessage)),
519
+ errors,
520
+ );
521
+ const stateLoad = this.core.stateLoads.get(scope.storageKey);
522
+ if (!stateLoad) {
523
+ const drain = this.storageDrains.get(scope.storageKey);
524
+ if (drain) {
525
+ try {
526
+ await drain;
527
+ } catch (error) {
528
+ this.core.appendUnexpectedErrors(errors, error);
529
+ }
530
+ } else {
531
+ errors.push(...await this.closeOrphanedResources(scope.storageKey));
532
+ }
533
+ } else {
534
+ try {
535
+ await this.drainStorage(
536
+ scope.storageKey,
537
+ stateLoad,
538
+ this.core.createScopeRetirementError(),
539
+ { scopeId, scope: scope.scope },
540
+ );
541
+ } catch (error) {
542
+ this.core.appendUnexpectedErrors(errors, error);
543
+ }
544
+ }
545
+ if (errors.length > 0) throw combineErrors(errors);
546
+ }
547
+
548
+ public drainStorage(
549
+ storageKey: string,
550
+ stateLoad: Promise<IStorageState>,
551
+ reason: FlexHarnessAbortError,
552
+ invocation?: { scopeId: string; scope: TScope },
553
+ ): Promise<void> {
554
+ const compactorController = this.storageCompactorLifecycleControllers.get(storageKey);
555
+ if (compactorController && !compactorController.signal.aborted) {
556
+ compactorController.abort(reason);
557
+ }
558
+ const existing = this.storageDrains.get(storageKey);
559
+ if (existing) return existing;
560
+ let completed = false;
561
+ let drain!: Promise<void>;
562
+ drain = this.drainStorageInternal(storageKey, stateLoad, reason, invocation).then(() => {
563
+ completed = true;
564
+ }).finally(() => {
565
+ if (completed && this.core.stateLoads.get(storageKey) === stateLoad) {
566
+ this.core.stateLoads.delete(storageKey);
567
+ if (this.storageCompactorLifecycleControllers.get(storageKey) === compactorController) {
568
+ this.storageCompactorLifecycleControllers.delete(storageKey);
569
+ }
570
+ }
571
+ if (this.storageDrains.get(storageKey) === drain) this.storageDrains.delete(storageKey);
572
+ });
573
+ this.storageDrains.set(storageKey, drain);
574
+ return drain;
575
+ }
576
+
577
+ private async drainStorageInternal(
578
+ storageKey: string,
579
+ stateLoad: Promise<IStorageState>,
580
+ reason: FlexHarnessAbortError,
581
+ invocation?: { scopeId: string; scope: TScope },
582
+ ): Promise<void> {
583
+ let state: IStorageState;
584
+ try {
585
+ state = await stateLoad;
586
+ } catch {
587
+ const orphanErrors = await this.closeOrphanedResources(storageKey);
588
+ if (orphanErrors.length > 0) throw combineErrors(orphanErrors);
589
+ return;
590
+ }
591
+ if (state.lifecycle !== 'fenced') state.lifecycle = 'retiring';
592
+ const configurationSettlement = this.core.configurations.abortSessionConfigurations(storageKey, reason);
593
+ if (configurationSettlement) await configurationSettlement;
594
+ for (const staged of [...state.sessionImports.values()]) {
595
+ if (staged.phase === 'staging') this.core.imports.releaseStagedSessionImport(staged);
596
+ }
597
+ const errors: unknown[] = [];
598
+ const listingSettlement = this.core.commands.abortSlashCommandListings(storageKey, reason);
599
+ if (listingSettlement) await listingSettlement;
600
+ await this.core.commands.abortSlashCommandExecutions(storageKey, reason, errors);
601
+ const detachedCleanupAttempts = this.core.providers.beginDetachedCleanupSettlement(state);
602
+ const contextFor = (sessionId: string) => invocation
603
+ ? this.core.compaction.createCompactorContext(invocation.scopeId, invocation.scope, storageKey, sessionId)
604
+ : undefined;
605
+ for (const stored of state.sessions.values()) {
606
+ this.core.compaction.abortCompactorLifecycle(stored, reason);
607
+ this.core.prompts.cancelStoredPromptQueue(
608
+ stored,
609
+ reason,
610
+ undefined,
611
+ contextFor(stored.session.sessionId),
612
+ );
613
+ }
614
+ for (const run of state.activeRuns.values()) {
615
+ if (run.phase !== 'finalizing' && run.phase !== 'promoting') {
616
+ this.core.runs.cancelRun(run, reason, contextFor(run.sessionId));
617
+ }
618
+ }
619
+ for (const pending of [...state.pendingPermissions.values()]) this.core.permissions.rejectPending(state, pending, reason);
620
+ const runResults = await Promise.allSettled([...state.activeRuns.values()].map((run) => run.completion));
621
+ for (const result of runResults) {
622
+ if (result.status === 'rejected') this.core.appendUnexpectedErrors(errors, result.reason);
623
+ }
624
+ errors.push(...await this.core.admission.settleDelegatedRunAdmissionOwners([
625
+ ...state.delegatedRunAdmissionOwners,
626
+ ]));
627
+ for (const stored of state.sessions.values()) {
628
+ try {
629
+ await this.abortStoredSession(
630
+ stored,
631
+ reason,
632
+ contextFor(stored.session.sessionId),
633
+ );
634
+ } catch (error) {
635
+ errors.push(this.core.projectOperationError(
636
+ error,
637
+ 'agentSession',
638
+ stored.session.scopeId,
639
+ stored.session.sessionId,
640
+ 'lifecycle-abort',
641
+ ));
642
+ }
643
+ }
644
+ await Promise.allSettled([...state.sessionInitializations.values()]);
645
+ errors.push(...await this.closeOrphanedResources(state.storageKey));
646
+ await state.scopeQueue;
647
+ await this.core.projects.awaitProjectManagementOperations(
648
+ state.storageKey,
649
+ [
650
+ ...state.sessions.keys(),
651
+ ...state.retainedSessionCleanups.keys(),
652
+ ],
653
+ );
654
+ await Promise.all([...state.sessions.values()].flatMap((stored) => [
655
+ stored.projectionQueue,
656
+ stored.permissionQueue,
657
+ ...(stored.promptQueueDrain ? [stored.promptQueueDrain] : []),
658
+ ]));
659
+ const tombstoneAttempts = [...state.tombstoneCleanups.entries()];
660
+ const tombstoneResults = await Promise.allSettled(
661
+ tombstoneAttempts.map(([, cleanup]) => cleanup),
662
+ );
663
+ for (const result of tombstoneResults) {
664
+ if (result.status === 'rejected') this.core.appendUnexpectedErrors(errors, result.reason);
665
+ }
666
+ const attemptedTombstones = new Set(tombstoneAttempts.map(([sessionId]) => sessionId));
667
+ errors.push(...await this.core.providers.settleDetachedCleanups(state, detachedCleanupAttempts));
668
+ for (const stored of state.sessions.values()) {
669
+ try {
670
+ await this.closeStoredSession(stored, contextFor(stored.session.sessionId));
671
+ } catch (error) {
672
+ this.core.appendUnexpectedErrors(errors, this.core.projectOperationError(
673
+ error,
674
+ 'toolCleanup',
675
+ stored.session.scopeId,
676
+ stored.session.sessionId,
677
+ 'lifecycle-close',
678
+ ));
679
+ }
680
+ const releaseContext = invocation ?? stored.compactorContext as
681
+ IFlexAgentContextInvocation<TScope> | undefined;
682
+ if (releaseContext) {
683
+ try {
684
+ await this.core.reversions.drainReversionReleases(
685
+ state,
686
+ stored,
687
+ releaseContext.scopeId,
688
+ releaseContext.scope,
689
+ true,
690
+ );
691
+ } catch (error) {
692
+ errors.push(error);
693
+ }
694
+ }
695
+ this.core.prompts.purgeStoredPromptQueue(stored);
696
+ }
697
+ const roots = this.core.deletion.orderTombstoneRootsChildFirst(
698
+ state,
699
+ [...new Set([...state.tombstones.values()].map((tombstone) =>
700
+ tombstone.rootSessionId ?? tombstone.sessionId))],
701
+ );
702
+ for (const rootSessionId of roots) {
703
+ if (attemptedTombstones.has(rootSessionId)) continue;
704
+ if (this.core.deletion.descendantTombstoneRoots(state, rootSessionId).length > 0) continue;
705
+ const retained = state.retainedSessionCleanups.get(rootSessionId);
706
+ try {
707
+ await this.core.deletion.finishTombstoneCleanup(
708
+ state,
709
+ rootSessionId,
710
+ invocation?.scopeId ?? retained?.stored.session.scopeId ?? state.scopeIdHint,
711
+ invocation,
712
+ );
713
+ } catch (error) {
714
+ this.core.appendUnexpectedErrors(errors, error);
715
+ }
716
+ }
717
+ errors.push(...state.fenceAdditionalErrors.splice(0));
718
+ if (errors.length > 0) throw combineErrors(errors);
719
+ state.lifecycle = 'retired';
720
+ state.sessions.clear();
721
+ state.retainedSessionCleanups.clear();
722
+ state.sessionDeletions.clear();
723
+ state.tombstoneCleanups.clear();
724
+ state.activeRuns.clear();
725
+ state.delegatedRunAdmissionOwners.clear();
726
+ state.pendingPermissions.clear();
727
+ state.initializingSessions.clear();
728
+ state.sessionInitializations.clear();
729
+ state.sessionImports.clear();
730
+ }
731
+
732
+ private async disposeInternal(): Promise<void> {
733
+ const pendingAdmissions = [...this.core.prompts.pendingPromptAdmissionOwners];
734
+ const pendingAdmissionReason = this.core.trustInternalError(new FlexHarnessAbortError(
735
+ 'The prompt admission was aborted because FlexHarness was disposed.',
736
+ ));
737
+ for (const pending of pendingAdmissions) {
738
+ if (!pending.controller.signal.aborted) pending.controller.abort(pendingAdmissionReason);
739
+ }
740
+ await Promise.all(pendingAdmissions.map((pending) => pending.settled));
741
+ const errors: unknown[] = [];
742
+ const listingSettlement = this.core.commands.abortSlashCommandListings(
743
+ undefined,
744
+ this.core.trustInternalError(new FlexHarnessAbortError(
745
+ 'The slash command listing was aborted because FlexHarness was disposed.',
746
+ )),
747
+ );
748
+ if (listingSettlement) await listingSettlement;
749
+ await this.core.commands.abortSlashCommandExecutions(
750
+ undefined,
751
+ this.core.trustInternalError(new FlexHarnessAbortError(
752
+ 'The slash command was aborted because FlexHarness was disposed.',
753
+ )),
754
+ errors,
755
+ );
756
+ const loads = [...this.core.stateLoads.entries()];
757
+ const results = await Promise.allSettled(loads.map(([storageKey, stateLoad]) =>
758
+ this.drainStorage(
759
+ storageKey,
760
+ stateLoad,
761
+ this.core.trustInternalError(new FlexHarnessAbortError(
762
+ 'The run was aborted because FlexHarness was disposed.',
763
+ )),
764
+ )));
765
+ for (const result of results) {
766
+ if (result.status === 'rejected') this.core.appendUnexpectedErrors(errors, result.reason);
767
+ }
768
+ const drainedStorageKeys = new Set(loads.map(([storageKey]) => storageKey));
769
+ for (const storageKey of this.orphanedResourceStorageKeys()) {
770
+ if (!drainedStorageKeys.has(storageKey)) {
771
+ errors.push(...await this.closeOrphanedResources(storageKey));
772
+ }
773
+ }
774
+ this.core.listeners.clear();
775
+ if (errors.length > 0) throw combineErrors(errors);
776
+ this.core.compaction.compactorInvocationContext.disable();
777
+ this.core.commands.slashCommandInvocationContext.disable();
778
+ this.core.commands.slashCommandActivityContext.disable();
779
+ this.core.stateLoads.clear();
780
+ this.core.scopeAdmissions.clear();
781
+ this.scopeRetirements.clear();
782
+ this.storageDrains.clear();
783
+ this.storageCompactorLifecycleControllers.clear();
784
+ this.core.configurations.sessionConfigurations.clear();
785
+ }
786
+ }