@modelprofile.com/flexharness 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,7 @@ import {
16
16
  errorToInfo,
17
17
  } from './errors.js';
18
18
  import type {
19
+ IFlexAgentContextInvocation,
19
20
  IFlexAgentSessionPolicy,
20
21
  IFlexAttachmentMessagePart,
21
22
  IFlexBackgroundExecution,
@@ -96,6 +97,7 @@ type TRunPhase =
96
97
 
97
98
  interface IStoredSessionState {
98
99
  storageKey: string;
100
+ compactorContext?: IFlexAgentContextInvocation<unknown>;
99
101
  session: IFlexSession;
100
102
  messages: IFlexMessage[];
101
103
  stagedTerminals: IFlexTerminalProjection[];
@@ -113,6 +115,7 @@ interface IStoredSessionState {
113
115
  executionContextCloseCompleted: boolean;
114
116
  jobStoreReleased: boolean;
115
117
  jobs?: NonNullable<plugins.IToolExecutionContext['jobs']>;
118
+ compactorLifecycleController: AbortController;
116
119
  promptQueue: IQueuedPrompt[];
117
120
  outstandingPromptsById: Map<string, IQueuedPrompt>;
118
121
  terminalPromptQueueEntries: Map<string, IFlexPromptQueueEntry>;
@@ -139,6 +142,12 @@ interface IOrphanedProviderRelease {
139
142
  release: () => Promise<void>;
140
143
  }
141
144
 
145
+ interface IOrphanedTombstoneCleanup {
146
+ storageKey: string;
147
+ sessionId: string;
148
+ completion: Promise<void>;
149
+ }
150
+
142
151
  interface IOrphanedExecutionContextOwner {
143
152
  storageKey: string;
144
153
  sessionId: string;
@@ -146,6 +155,7 @@ interface IOrphanedExecutionContextOwner {
146
155
 
147
156
  interface IStorageState {
148
157
  storageKey: string;
158
+ compactorLifecycleController: AbortController;
149
159
  scopeIdHint: string;
150
160
  revision: number;
151
161
  sessions: Map<string, IStoredSessionState>;
@@ -167,6 +177,8 @@ interface IStorageState {
167
177
  interface IActiveRun {
168
178
  state: IStorageState;
169
179
  stored: IStoredSessionState;
180
+ originCompactorContext: IFlexAgentContextInvocation<unknown>;
181
+ deferredCompactorContext?: IFlexAgentContextInvocation<unknown>;
170
182
  scopeId: string;
171
183
  scope: unknown;
172
184
  sessionId: string;
@@ -694,7 +706,9 @@ function resolvePromptQueueLimits(
694
706
  return resolved;
695
707
  }
696
708
 
697
- function normalizeAgentSessionPolicy(policy: IFlexAgentSessionPolicy = {}): IFlexAgentSessionPolicy {
709
+ function normalizeAgentSessionPolicy<TScope>(
710
+ policy: IFlexAgentSessionPolicy<TScope> = {},
711
+ ): IFlexAgentSessionPolicy<TScope> {
698
712
  return {
699
713
  ...(policy.contextBuilder === undefined ? {} : { contextBuilder: policy.contextBuilder }),
700
714
  ...(policy.contextCompactor === undefined ? {} : { contextCompactor: policy.contextCompactor }),
@@ -723,7 +737,7 @@ export class FlexHarness<TScope = unknown> {
723
737
  private readonly toolProvider: IFlexHarnessOptions<TScope>['toolProvider'];
724
738
  private readonly executionContextProvider: IFlexHarnessOptions<TScope>['executionContextProvider'];
725
739
  private readonly stores: IFlexHarnessStores;
726
- private readonly agentSessionPolicy: IFlexAgentSessionPolicy;
740
+ private readonly agentSessionPolicy: IFlexAgentSessionPolicy<TScope>;
727
741
  private readonly toolOutputLimits: Required<NonNullable<IFlexHarnessOptions<TScope>['toolOutputLimits']>>;
728
742
  private readonly callbackLimits: Required<IFlexCallbackLimits>;
729
743
  private readonly promptQueueLimits: Required<IFlexPromptQueueLimits>;
@@ -732,6 +746,7 @@ export class FlexHarness<TScope = unknown> {
732
746
  private readonly scopeAdmissions = new Map<string, IScopeAdmissionState>();
733
747
  private readonly scopeRetirements = new Map<string, Promise<void>>();
734
748
  private readonly storageDrains = new Map<string, Promise<void>>();
749
+ private readonly storageCompactorLifecycleControllers = new Map<string, AbortController>();
735
750
  private orphanedResourceQueue: Promise<void> = Promise.resolve();
736
751
  private readonly listeners = new Set<TFlexHarnessEventListener>();
737
752
  private readonly trustedInternalErrors = new WeakSet<object>();
@@ -741,7 +756,15 @@ export class FlexHarness<TScope = unknown> {
741
756
  IOrphanedExecutionContextOwner
742
757
  >();
743
758
  private readonly orphanedProviderReleases = new Map<string, IOrphanedProviderRelease>();
759
+ private readonly orphanedTombstoneCleanups = new Map<string, IOrphanedTombstoneCleanup>();
744
760
  private readonly pendingPromptAdmissionOwners = new Set<IPendingPromptAdmission>();
761
+ private readonly compactorInvocationContext = new plugins.AsyncLocalStorage<
762
+ IFlexAgentContextInvocation<TScope>
763
+ >();
764
+ private readonly deferredCompactorContexts = new WeakMap<
765
+ IFlexAgentContextInvocation<unknown>,
766
+ IFlexAgentContextInvocation<unknown>
767
+ >();
745
768
  private sequence = 0;
746
769
  private promptQueueSequence = 0;
747
770
  private pendingPromptAdmissions = 0;
@@ -876,7 +899,12 @@ export class FlexHarness<TScope = unknown> {
876
899
  resolved.state.tombstones.set(sessionId, tombstone);
877
900
  }, true);
878
901
  tombstoneCommitted = true;
879
- await this.finishTombstoneCleanup(resolved.state, sessionId, scopeId);
902
+ await this.finishTombstoneCleanup(
903
+ resolved.state,
904
+ sessionId,
905
+ scopeId,
906
+ resolved.scope.scope,
907
+ );
880
908
  domainCleanupCompleted = true;
881
909
  } catch (cleanupError) {
882
910
  const projectedCleanup = this.projectOperationError(
@@ -898,6 +926,7 @@ export class FlexHarness<TScope = unknown> {
898
926
  this.trustInternalError(new FlexHarnessAbortError(
899
927
  'The session namespace was fenced after uncertain creation cleanup.',
900
928
  )),
929
+ { scopeId, scope: resolved.scope.scope },
901
930
  );
902
931
  } catch {
903
932
  // The projected creation and cleanup errors remain safe for the caller.
@@ -950,12 +979,12 @@ export class FlexHarness<TScope = unknown> {
950
979
  }
951
980
 
952
981
  public async deleteSession(scopeId: string, sessionId: string): Promise<void> {
953
- const { state } = await this.resolveState(scopeId);
982
+ const { scope, state } = await this.resolveState(scopeId);
954
983
  validateIdentifier(sessionId, 'sessionId');
955
984
  const existingDeletion = state.sessionDeletions.get(sessionId);
956
985
  if (existingDeletion) return existingDeletion;
957
986
  let deletion!: Promise<void>;
958
- deletion = this.deleteSessionInternal(state, scopeId, sessionId).finally(() => {
987
+ deletion = this.deleteSessionInternal(state, scopeId, scope.scope, sessionId).finally(() => {
959
988
  if (state.sessionDeletions.get(sessionId) === deletion) {
960
989
  state.sessionDeletions.delete(sessionId);
961
990
  }
@@ -967,12 +996,13 @@ export class FlexHarness<TScope = unknown> {
967
996
  private async deleteSessionInternal(
968
997
  state: IStorageState,
969
998
  scopeId: string,
999
+ scope: TScope,
970
1000
  sessionId: string,
971
1001
  ): Promise<void> {
972
1002
  const existingTombstone = state.tombstones.get(sessionId);
973
1003
  if (existingTombstone) {
974
1004
  try {
975
- await this.finishTombstoneCleanup(state, sessionId, scopeId);
1005
+ await this.finishTombstoneCleanup(state, sessionId, scopeId, scope);
976
1006
  } catch (error) {
977
1007
  throw this.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-delete');
978
1008
  }
@@ -995,7 +1025,7 @@ export class FlexHarness<TScope = unknown> {
995
1025
  });
996
1026
  });
997
1027
  try {
998
- await this.finishTombstoneCleanup(state, sessionId, scopeId);
1028
+ await this.finishTombstoneCleanup(state, sessionId, scopeId, scope);
999
1029
  } catch (error) {
1000
1030
  throw this.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-delete');
1001
1031
  }
@@ -1177,7 +1207,7 @@ export class FlexHarness<TScope = unknown> {
1177
1207
  ): Promise<boolean> {
1178
1208
  validateIdentifier(queueId, 'queueId');
1179
1209
  requireTransferIdentifier(queueId, 'queueId');
1180
- const { state } = await this.resolveState(scopeId);
1210
+ const { scope, state } = await this.resolveState(scopeId);
1181
1211
  const stored = this.requireSession(state, sessionId);
1182
1212
  const queued = stored.outstandingPromptsById.get(queueId);
1183
1213
  if (!queued) {
@@ -1187,6 +1217,7 @@ export class FlexHarness<TScope = unknown> {
1187
1217
  return this.cancelQueuedPrompt(
1188
1218
  queued,
1189
1219
  this.trustInternalError(new FlexHarnessAbortError('The queued prompt was cancelled.')),
1220
+ this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
1190
1221
  );
1191
1222
  }
1192
1223
 
@@ -1196,20 +1227,28 @@ export class FlexHarness<TScope = unknown> {
1196
1227
  scheduleKey: string,
1197
1228
  ): Promise<boolean> {
1198
1229
  validateIdentifier(scheduleKey, 'scheduleKey');
1199
- const { state } = await this.resolveState(scopeId);
1230
+ const { scope, state } = await this.resolveState(scopeId);
1200
1231
  const stored = this.requireSession(state, sessionId);
1201
1232
  const queued = [...stored.outstandingPromptsById.values()]
1202
1233
  .find((entry) => entry.scheduleKey === scheduleKey);
1203
1234
  if (!queued) return false;
1204
1235
  const cancellation = this.trustInternalError(new FlexHarnessAbortError('The scheduled run was cancelled.'));
1205
- return this.cancelQueuedPrompt(queued, cancellation);
1236
+ return this.cancelQueuedPrompt(
1237
+ queued,
1238
+ cancellation,
1239
+ this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
1240
+ );
1206
1241
  }
1207
1242
 
1208
1243
  public async abort(scopeId: string, sessionId: string): Promise<boolean> {
1209
- const { state } = await this.resolveState(scopeId);
1244
+ const { scope, state } = await this.resolveState(scopeId);
1210
1245
  const run = state.activeRuns.get(sessionId);
1211
1246
  if (!run || run.phase === 'finalizing' || run.phase === 'promoting') return false;
1212
- this.cancelRun(run, this.trustInternalError(new FlexHarnessAbortError()));
1247
+ this.cancelRun(
1248
+ run,
1249
+ this.trustInternalError(new FlexHarnessAbortError()),
1250
+ this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
1251
+ );
1213
1252
  return true;
1214
1253
  }
1215
1254
 
@@ -1233,12 +1272,16 @@ export class FlexHarness<TScope = unknown> {
1233
1272
  decision: TFlexPermissionDecision,
1234
1273
  ): Promise<void> {
1235
1274
  validateIdentifier(permissionId, 'permissionId');
1236
- const { state } = await this.resolveState(scopeId);
1275
+ const { scope, state } = await this.resolveState(scopeId);
1237
1276
  const pending = state.pendingPermissions.get(permissionId);
1238
1277
  if (!pending || pending.request.sessionId !== sessionId) {
1239
1278
  throw new FlexHarnessNotFoundError('Permission', permissionId);
1240
1279
  }
1241
- const response = pending.responseQueue.then(() => this.applyPermissionResponse(state, pending, decision));
1280
+ const context = this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId);
1281
+ const response = pending.responseQueue.then(() => this.withCompactorContext(
1282
+ context,
1283
+ () => this.applyPermissionResponse(state, pending, decision, context),
1284
+ ));
1242
1285
  pending.responseQueue = response.then(() => undefined, () => undefined);
1243
1286
  return response;
1244
1287
  }
@@ -1275,7 +1318,7 @@ export class FlexHarness<TScope = unknown> {
1275
1318
  if (!reconciliation || typeof reconciliation !== 'object' || Array.isArray(reconciliation)) {
1276
1319
  throw new FlexHarnessValidationError('Tool execution reconciliation must be a plain object.');
1277
1320
  }
1278
- const { state } = await this.resolveState(scopeId);
1321
+ const { scope, state } = await this.resolveState(scopeId);
1279
1322
  const stored = this.requireSession(state, sessionId);
1280
1323
  this.assertStateAcceptingWork(state);
1281
1324
  let canonical: plugins.TAgentToolExecutionReconciliationOptions;
@@ -1299,7 +1342,10 @@ export class FlexHarness<TScope = unknown> {
1299
1342
  throw new FlexHarnessValidationError('Unknown tool execution reconciliation.');
1300
1343
  }
1301
1344
  try {
1302
- await stored.agentSession.reconcileToolExecution(intentId, canonical);
1345
+ await this.withCompactorContext(
1346
+ this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
1347
+ () => stored.agentSession.reconcileToolExecution(intentId, canonical),
1348
+ );
1303
1349
  } catch (error) {
1304
1350
  throw this.projectOperationError(error, 'agentSession', scopeId, sessionId, 'tool-reconciliation');
1305
1351
  }
@@ -1311,22 +1357,28 @@ export class FlexHarness<TScope = unknown> {
1311
1357
  event: IJsonObject & { type: string },
1312
1358
  ): Promise<void> {
1313
1359
  this.validateRuntimeEvent(event);
1314
- const { state } = await this.resolveState(scopeId);
1360
+ const { scope, state } = await this.resolveState(scopeId);
1315
1361
  const stored = this.requireSession(state, sessionId);
1316
1362
  this.assertStateAcceptingWork(state);
1317
1363
  try {
1318
- await stored.agentSession.pushRuntimeEvent(cloneSerializable(event));
1364
+ await this.withCompactorContext(
1365
+ this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
1366
+ () => stored.agentSession.pushRuntimeEvent(cloneSerializable(event)),
1367
+ );
1319
1368
  } catch (error) {
1320
1369
  throw this.projectOperationError(error, 'agentSession', scopeId, sessionId, 'runtime-event');
1321
1370
  }
1322
1371
  }
1323
1372
 
1324
1373
  public async compactSession(scopeId: string, sessionId: string): Promise<void> {
1325
- const { state } = await this.resolveState(scopeId);
1374
+ const { scope, state } = await this.resolveState(scopeId);
1326
1375
  const stored = this.requireSession(state, sessionId);
1327
1376
  this.assertStateAcceptingWork(state);
1328
1377
  try {
1329
- await stored.agentSession.compact();
1378
+ await this.withCompactorContext(
1379
+ this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
1380
+ () => stored.agentSession.compact(),
1381
+ );
1330
1382
  } catch (error) {
1331
1383
  throw this.projectOperationError(error, 'agentSession', scopeId, sessionId, 'compaction');
1332
1384
  }
@@ -1403,11 +1455,14 @@ export class FlexHarness<TScope = unknown> {
1403
1455
  ): Promise<void> {
1404
1456
  validateIdentifier(executionId, 'executionId');
1405
1457
  requireTransferIdentifier(executionId, 'executionId');
1406
- const { state } = await this.resolveState(scopeId);
1458
+ const { scope, state } = await this.resolveState(scopeId);
1407
1459
  const stored = this.requireSession(state, sessionId);
1408
1460
  this.assertStateAcceptingWork(state);
1409
1461
  try {
1410
- await stored.agentSession.abortBackgroundExecution(executionId);
1462
+ await this.withCompactorContext(
1463
+ this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
1464
+ () => stored.agentSession.abortBackgroundExecution(executionId),
1465
+ );
1411
1466
  } catch (error) {
1412
1467
  throw this.projectOperationError(error, 'agentSession', scopeId, sessionId, 'background-abort');
1413
1468
  }
@@ -1636,6 +1691,12 @@ export class FlexHarness<TScope = unknown> {
1636
1691
  return {
1637
1692
  state: queued.state,
1638
1693
  stored: queued.stored,
1694
+ originCompactorContext: this.createCompactorContext(
1695
+ queued.scopeId,
1696
+ queued.scope as TScope,
1697
+ queued.state.storageKey,
1698
+ queued.sessionId,
1699
+ ),
1639
1700
  scopeId: queued.scopeId,
1640
1701
  scope: queued.scope,
1641
1702
  sessionId: queued.sessionId,
@@ -1676,11 +1737,14 @@ export class FlexHarness<TScope = unknown> {
1676
1737
  const options = queued.options!;
1677
1738
  let projectionReserved = false;
1678
1739
  try {
1679
- run.transaction = await run.stored.agentSession.beginGeneration(
1680
- cloneSerializable(prompt.modelMessage.content) as Parameters<
1681
- plugins.IAgentSession['beginGeneration']
1682
- >[0],
1683
- { generationId: run.runId },
1740
+ run.transaction = await this.withRunCompactorContext(
1741
+ run,
1742
+ () => run.stored.agentSession.beginGeneration(
1743
+ cloneSerializable(prompt.modelMessage.content) as Parameters<
1744
+ plugins.IAgentSession['beginGeneration']
1745
+ >[0],
1746
+ { generationId: run.runId },
1747
+ ),
1684
1748
  );
1685
1749
  this.assertPromptPromotion(queued, run);
1686
1750
  const reservation = this.createReservation(run, prompt);
@@ -1759,13 +1823,13 @@ export class FlexHarness<TScope = unknown> {
1759
1823
  prepare: (context: { generationId: string; abortSignal: AbortSignal }) =>
1760
1824
  this.prepareGeneration(run, options, context.abortSignal),
1761
1825
  };
1762
- const generation = run.scheduleKey
1826
+ const generation = this.withRunCompactorContext(run, () => run.scheduleKey
1763
1827
  ? run.stored.agentSession.scheduleGenerate({
1764
1828
  key: run.scheduleKey,
1765
1829
  debounceMs: run.debounceMs,
1766
1830
  ...generateOptions,
1767
1831
  })
1768
- : run.stored.agentSession.generate(generateOptions);
1832
+ : run.stored.agentSession.generate(generateOptions));
1769
1833
  const execution = this.executeRun(run, generation).then(
1770
1834
  (result) => this.finishQueuedPrompt(queued, 'completed', undefined, result),
1771
1835
  (error) => this.finishQueuedPrompt(
@@ -1853,7 +1917,11 @@ export class FlexHarness<TScope = unknown> {
1853
1917
  }
1854
1918
  }
1855
1919
 
1856
- private cancelQueuedPrompt(queued: IQueuedPrompt, reason: FlexHarnessAbortError): boolean {
1920
+ private cancelQueuedPrompt(
1921
+ queued: IQueuedPrompt,
1922
+ reason: FlexHarnessAbortError,
1923
+ context?: IFlexAgentContextInvocation<TScope>,
1924
+ ): boolean {
1857
1925
  if (queued.stored.outstandingPromptsById.get(queued.queueId) !== queued) return false;
1858
1926
  if (queued.status === 'queued') {
1859
1927
  this.finishQueuedPrompt(queued, 'cancelled', reason);
@@ -1863,7 +1931,7 @@ export class FlexHarness<TScope = unknown> {
1863
1931
  const run = queued.state.activeRuns.get(queued.sessionId);
1864
1932
  if (!run || run.queueId !== queued.queueId) return false;
1865
1933
  if (run.phase === 'finalizing' || run.phase === 'promoting') return false;
1866
- this.cancelRun(run, reason);
1934
+ this.cancelRun(run, reason, context);
1867
1935
  return true;
1868
1936
  }
1869
1937
 
@@ -1871,10 +1939,15 @@ export class FlexHarness<TScope = unknown> {
1871
1939
  stored: IStoredSessionState,
1872
1940
  reason: FlexHarnessAbortError,
1873
1941
  excludedQueueId?: string,
1942
+ context?: IFlexAgentContextInvocation<unknown>,
1874
1943
  ): void {
1875
1944
  for (const queued of [...stored.outstandingPromptsById.values()]) {
1876
1945
  if (queued.queueId === excludedQueueId) continue;
1877
- this.cancelQueuedPrompt(queued, reason);
1946
+ this.cancelQueuedPrompt(
1947
+ queued,
1948
+ reason,
1949
+ context as IFlexAgentContextInvocation<TScope> | undefined,
1950
+ );
1878
1951
  }
1879
1952
  }
1880
1953
 
@@ -1908,7 +1981,10 @@ export class FlexHarness<TScope = unknown> {
1908
1981
  if (run.controller.signal.aborted) throw run.controller.signal.reason;
1909
1982
  run.phase = 'finalizing';
1910
1983
  try {
1911
- await run.stored.agentSession.finalizeGeneration(run.transaction!, 'accepted');
1984
+ await this.withRunCompactorContext(
1985
+ run,
1986
+ () => run.stored.agentSession.finalizeGeneration(run.transaction!, 'accepted'),
1987
+ );
1912
1988
  } catch (error) {
1913
1989
  const projected = this.projectExternalError(run, error, 'agentSession');
1914
1990
  await this.fenceNamespace(run.state, run, projected);
@@ -1978,7 +2054,10 @@ export class FlexHarness<TScope = unknown> {
1978
2054
  run.phase = 'finalizing';
1979
2055
  const desiredOutcome: TCanonicalOutcome = cancelled || !generated ? 'interrupted' : 'rejected';
1980
2056
  try {
1981
- await run.stored.agentSession.finalizeGeneration(run.transaction!, desiredOutcome);
2057
+ await this.withRunCompactorContext(
2058
+ run,
2059
+ () => run.stored.agentSession.finalizeGeneration(run.transaction!, desiredOutcome),
2060
+ );
1982
2061
  } catch (finalizationError) {
1983
2062
  errors.push(this.projectExternalError(run, finalizationError, 'agentSession'));
1984
2063
  }
@@ -2352,11 +2431,14 @@ export class FlexHarness<TScope = unknown> {
2352
2431
  private emitTerminalProjection(run: IActiveRun, terminal: IFlexTerminalProjection): void {
2353
2432
  for (const part of terminal.assistantMessage.parts) {
2354
2433
  const callbackPart = run.callbackParts.find((entry) => entry.partId === part.partId);
2355
- if (
2356
- (part.type === 'reasoning' || part.type === 'tool')
2357
- && callbackPart?.type === part.type
2358
- && callbackPart.status === 'running'
2359
- ) {
2434
+ const shouldComplete = part.type === 'text'
2435
+ ? callbackPart?.type === 'text'
2436
+ : part.type === 'reasoning'
2437
+ ? callbackPart?.type === 'reasoning' && callbackPart.status === 'running'
2438
+ : part.type === 'tool'
2439
+ ? callbackPart?.type === 'tool' && callbackPart.status === 'running'
2440
+ : false;
2441
+ if (shouldComplete) {
2360
2442
  this.emitPartEvent(run, 'part.completed', part);
2361
2443
  }
2362
2444
  }
@@ -2617,6 +2699,7 @@ export class FlexHarness<TScope = unknown> {
2617
2699
  state: IStorageState,
2618
2700
  pending: IPendingPermission,
2619
2701
  decision: TFlexPermissionDecision,
2702
+ context: IFlexAgentContextInvocation<TScope>,
2620
2703
  ): Promise<void> {
2621
2704
  if (pending.settled) {
2622
2705
  throw new FlexHarnessPermissionStateError(`Permission "${pending.request.permissionId}" has already been resolved.`);
@@ -2714,7 +2797,7 @@ export class FlexHarness<TScope = unknown> {
2714
2797
  const rejection = this.trustInternalError(
2715
2798
  new FlexHarnessPermissionRejectedError(pending.request.permissionId),
2716
2799
  );
2717
- this.abortRunInternally(pending.run, rejection);
2800
+ this.abortRunInternally(pending.run, rejection, context);
2718
2801
  pending.reject(rejection);
2719
2802
  } else {
2720
2803
  pending.resolve();
@@ -2818,19 +2901,50 @@ export class FlexHarness<TScope = unknown> {
2818
2901
  pending.reject(error);
2819
2902
  }
2820
2903
 
2821
- private cancelRun(run: IActiveRun, reason: FlexHarnessAbortError): void {
2822
- if (run.internalFailure === undefined) run.ownerCancellation ??= reason;
2823
- this.rejectRunPermissions(run.state, run, reason);
2824
- if (!run.controller.signal.aborted) run.controller.abort(reason);
2825
- if (run.phase === 'admitting') return;
2826
- if (run.scheduleKey) run.stored.agentSession.cancelScheduledGeneration(run.scheduleKey, reason);
2827
- else run.stored.agentSession.abortCurrentGeneration(reason);
2904
+ private cancelRun(
2905
+ run: IActiveRun,
2906
+ reason: FlexHarnessAbortError,
2907
+ context?: IFlexAgentContextInvocation<TScope>,
2908
+ ): void {
2909
+ const invocationContext = context ?? this.createRunCompactorContext(run);
2910
+ if (run.internalFailure === undefined && run.ownerCancellation === undefined) {
2911
+ run.ownerCancellation = reason;
2912
+ run.deferredCompactorContext = invocationContext;
2913
+ this.deferredCompactorContexts.set(run.originCompactorContext, invocationContext);
2914
+ }
2915
+ this.withCompactorContext(
2916
+ (run.deferredCompactorContext ?? invocationContext) as IFlexAgentContextInvocation<TScope>,
2917
+ () => {
2918
+ this.rejectRunPermissions(run.state, run, reason);
2919
+ if (!run.controller.signal.aborted) run.controller.abort(reason);
2920
+ if (run.phase === 'admitting') return;
2921
+ if (run.scheduleKey) {
2922
+ run.stored.agentSession.cancelScheduledGeneration(run.scheduleKey, reason);
2923
+ } else {
2924
+ run.stored.agentSession.abortCurrentGeneration(reason);
2925
+ }
2926
+ },
2927
+ );
2828
2928
  }
2829
2929
 
2830
- private abortRunInternally(run: IActiveRun, error: unknown): void {
2831
- run.internalFailure ??= error ?? new Error('Internal FlexHarness run failure.');
2832
- if (!run.controller.signal.aborted) run.controller.abort(run.internalFailure);
2833
- run.stored.agentSession.abortCurrentGeneration(run.internalFailure);
2930
+ private abortRunInternally(
2931
+ run: IActiveRun,
2932
+ error: unknown,
2933
+ context?: IFlexAgentContextInvocation<TScope>,
2934
+ ): void {
2935
+ const invocationContext = context ?? this.createRunCompactorContext(run);
2936
+ if (run.internalFailure === undefined) {
2937
+ run.internalFailure = error ?? new Error('Internal FlexHarness run failure.');
2938
+ run.deferredCompactorContext = invocationContext;
2939
+ this.deferredCompactorContexts.set(run.originCompactorContext, invocationContext);
2940
+ }
2941
+ this.withCompactorContext(
2942
+ (run.deferredCompactorContext ?? invocationContext) as IFlexAgentContextInvocation<TScope>,
2943
+ () => {
2944
+ if (!run.controller.signal.aborted) run.controller.abort(run.internalFailure);
2945
+ run.stored.agentSession.abortCurrentGeneration(run.internalFailure);
2946
+ },
2947
+ );
2834
2948
  }
2835
2949
 
2836
2950
  private async rollbackAdmission(
@@ -2841,7 +2955,10 @@ export class FlexHarness<TScope = unknown> {
2841
2955
  const errors: unknown[] = [];
2842
2956
  if (run.transaction) {
2843
2957
  try {
2844
- await run.stored.agentSession.finalizeGeneration(run.transaction, 'interrupted');
2958
+ await this.withRunCompactorContext(
2959
+ run,
2960
+ () => run.stored.agentSession.finalizeGeneration(run.transaction!, 'interrupted'),
2961
+ );
2845
2962
  } catch (error) {
2846
2963
  errors.push(this.projectExternalError(run, error, 'agentSession'));
2847
2964
  }
@@ -3068,6 +3185,7 @@ export class FlexHarness<TScope = unknown> {
3068
3185
  storageKey: string,
3069
3186
  scopeId: string,
3070
3187
  scope: TScope,
3188
+ compactorLifecycleController: AbortController,
3071
3189
  ): Promise<IStorageState> {
3072
3190
  const snapshot = cloneSerializable(await this.stores.scopes.load(storageKey) ?? {
3073
3191
  schemaVersion: 1,
@@ -3077,6 +3195,7 @@ export class FlexHarness<TScope = unknown> {
3077
3195
  } satisfies IFlexScopeSnapshot);
3078
3196
  const state: IStorageState = {
3079
3197
  storageKey,
3198
+ compactorLifecycleController,
3080
3199
  scopeIdHint: scopeId,
3081
3200
  revision: snapshot.revision,
3082
3201
  sessions: new Map(),
@@ -3164,6 +3283,7 @@ export class FlexHarness<TScope = unknown> {
3164
3283
  agentEventStoreReleased: true,
3165
3284
  executionContextCloseCompleted: true,
3166
3285
  jobStoreReleased: true,
3286
+ compactorLifecycleController: new AbortController(),
3167
3287
  promptQueue: [],
3168
3288
  outstandingPromptsById: new Map(),
3169
3289
  terminalPromptQueueEntries: new Map(),
@@ -3178,6 +3298,12 @@ export class FlexHarness<TScope = unknown> {
3178
3298
  scope: TScope,
3179
3299
  ): Promise<IStoredSessionState> {
3180
3300
  const sessionId = metadata.sessionId;
3301
+ const compactorContext = this.createCompactorContext(
3302
+ scopeId,
3303
+ scope,
3304
+ state.storageKey,
3305
+ sessionId,
3306
+ );
3181
3307
  const [projectionResult, permissionResult, eventStoreResult, jobStoreResult] = await Promise.allSettled([
3182
3308
  this.stores.projections.load(state.storageKey, sessionId),
3183
3309
  this.stores.permissions.load(state.storageKey, sessionId),
@@ -3216,13 +3342,16 @@ export class FlexHarness<TScope = unknown> {
3216
3342
  let executionContextHandle: IFlexExecutionContextHandle | undefined;
3217
3343
  try {
3218
3344
  if (this.executionContextProvider) {
3219
- executionContextHandle = await this.executionContextProvider.provideExecutionContext({
3220
- scopeId,
3221
- scope,
3222
- storageKey: state.storageKey,
3223
- sessionId,
3224
- jobStore,
3225
- });
3345
+ executionContextHandle = await this.withCompactorContext(
3346
+ compactorContext,
3347
+ () => this.executionContextProvider!.provideExecutionContext({
3348
+ scopeId,
3349
+ scope,
3350
+ storageKey: state.storageKey,
3351
+ sessionId,
3352
+ jobStore,
3353
+ }),
3354
+ );
3226
3355
  if (
3227
3356
  executionContextHandle
3228
3357
  && (!executionContextHandle.context
@@ -3233,6 +3362,7 @@ export class FlexHarness<TScope = unknown> {
3233
3362
  }
3234
3363
  const stored: IStoredSessionState = {
3235
3364
  storageKey: state.storageKey,
3365
+ compactorContext,
3236
3366
  session: cloneSerializable(metadata),
3237
3367
  messages: cloneSerializable(projection?.messages ?? []),
3238
3368
  stagedTerminals: cloneSerializable(projection?.stagedTerminals ?? []),
@@ -3250,21 +3380,75 @@ export class FlexHarness<TScope = unknown> {
3250
3380
  executionContextCloseCompleted: executionContextHandle?.close === undefined,
3251
3381
  jobStoreReleased: this.stores.jobs.releaseSession === undefined,
3252
3382
  jobs: executionContextHandle?.context.jobs,
3383
+ compactorLifecycleController: new AbortController(),
3253
3384
  promptQueue: [],
3254
3385
  outstandingPromptsById: new Map(),
3255
3386
  terminalPromptQueueEntries: new Map(),
3256
3387
  outstandingPromptBytes: 0,
3257
3388
  };
3389
+ const {
3390
+ contextCompactor,
3391
+ ...agentSessionPolicy
3392
+ } = this.agentSessionPolicy;
3393
+ const executionContext = executionContextHandle?.context;
3394
+ const jobContext = executionContext?.jobs;
3395
+ const contextualExecutionContext = executionContext && jobContext?.subscribe
3396
+ ? {
3397
+ ...executionContext,
3398
+ jobs: {
3399
+ start: jobContext.start.bind(jobContext),
3400
+ get: jobContext.get.bind(jobContext),
3401
+ list: jobContext.list.bind(jobContext),
3402
+ ...(jobContext.abort ? { abort: jobContext.abort.bind(jobContext) } : {}),
3403
+ subscribe: (listener: Parameters<NonNullable<typeof jobContext.subscribe>>[0]) =>
3404
+ jobContext.subscribe!((event) =>
3405
+ this.withCompactorContext(compactorContext, () => listener(event))),
3406
+ },
3407
+ }
3408
+ : executionContext;
3258
3409
  const agentSessionOptions: plugins.IAgentSessionOptions & {
3259
3410
  transactionOutcomeErrorProjector: (error: unknown) => string;
3260
3411
  } = {
3261
- ...this.agentSessionPolicy,
3412
+ ...agentSessionPolicy,
3262
3413
  sessionId,
3263
3414
  eventStore,
3264
- executionContext: executionContextHandle?.context,
3415
+ executionContext: contextualExecutionContext,
3265
3416
  contextBuilder: ({ events }) => hydrateAgentMessages(
3266
3417
  (this.agentSessionPolicy.contextBuilder ?? ((options) => plugins.buildModelMessages(options.events)))({ events }),
3267
3418
  ),
3419
+ ...(contextCompactor
3420
+ ? {
3421
+ contextCompactor: async (messages, events, options) => {
3422
+ const ambientContext = this.compactorInvocationContext.getStore();
3423
+ const invocationContext = ambientContext
3424
+ ? (this.deferredCompactorContexts.get(ambientContext) ?? ambientContext) as
3425
+ IFlexAgentContextInvocation<TScope>
3426
+ : undefined;
3427
+ if (
3428
+ !invocationContext
3429
+ || invocationContext.storageKey !== state.storageKey
3430
+ || invocationContext.sessionId !== sessionId
3431
+ ) {
3432
+ throw new Error('Agent context compaction is missing its exact FlexHarness invocation context.');
3433
+ }
3434
+ return contextCompactor(messages, events, {
3435
+ ...options,
3436
+ ...invocationContext,
3437
+ scope: invocationContext.scope as TScope,
3438
+ abortSignal: options.abortSignal
3439
+ ? AbortSignal.any([
3440
+ options.abortSignal,
3441
+ state.compactorLifecycleController.signal,
3442
+ stored.compactorLifecycleController.signal,
3443
+ ])
3444
+ : AbortSignal.any([
3445
+ state.compactorLifecycleController.signal,
3446
+ stored.compactorLifecycleController.signal,
3447
+ ]),
3448
+ });
3449
+ },
3450
+ }
3451
+ : {}),
3268
3452
  transactionOutcomeErrorProjector: () => 'The model operation failed.',
3269
3453
  onToken: (delta) => this.onTextDelta(state, sessionId, delta),
3270
3454
  onReasoningStart: (id) => this.onReasoningStart(state, sessionId, id),
@@ -3273,7 +3457,10 @@ export class FlexHarness<TScope = unknown> {
3273
3457
  onToolCallStart: (event) => this.onToolStart(state, sessionId, event),
3274
3458
  onToolCallFinish: (event) => this.onToolFinish(state, sessionId, event),
3275
3459
  };
3276
- stored.agentSession = await plugins.AgentSession.create(agentSessionOptions);
3460
+ stored.agentSession = await this.withCompactorContext(
3461
+ compactorContext,
3462
+ () => plugins.AgentSession.create(agentSessionOptions),
3463
+ );
3277
3464
  return stored;
3278
3465
  } catch (error) {
3279
3466
  const cleanupErrors: unknown[] = [];
@@ -3744,11 +3931,12 @@ export class FlexHarness<TScope = unknown> {
3744
3931
  state: IStorageState,
3745
3932
  sessionId: string,
3746
3933
  scopeId: string,
3934
+ scope?: TScope,
3747
3935
  ): Promise<void> {
3748
3936
  const existing = state.tombstoneCleanups.get(sessionId);
3749
3937
  if (existing) return existing;
3750
3938
  let cleanup!: Promise<void>;
3751
- cleanup = this.finishTombstoneCleanupInternal(state, sessionId, scopeId).finally(() => {
3939
+ cleanup = this.finishTombstoneCleanupInternal(state, sessionId, scopeId, scope).finally(() => {
3752
3940
  if (state.tombstoneCleanups.get(sessionId) === cleanup) {
3753
3941
  state.tombstoneCleanups.delete(sessionId);
3754
3942
  }
@@ -3761,17 +3949,28 @@ export class FlexHarness<TScope = unknown> {
3761
3949
  state: IStorageState,
3762
3950
  sessionId: string,
3763
3951
  scopeId: string,
3952
+ scope?: TScope,
3764
3953
  ): Promise<void> {
3765
3954
  const retained = state.retainedSessionCleanups.get(sessionId);
3766
3955
  if (retained) {
3767
3956
  const errors: unknown[] = [];
3768
3957
  const reason = this.trustInternalError(new FlexHarnessAbortError('The session was deleted.'));
3769
- this.cancelStoredPromptQueue(retained.stored, reason);
3958
+ const invocation = scope === undefined
3959
+ ? retained.stored.compactorContext
3960
+ : this.createCompactorContext(scopeId, scope, state.storageKey, sessionId);
3961
+ this.abortCompactorLifecycle(retained.stored, reason);
3962
+ this.cancelStoredPromptQueue(retained.stored, reason, undefined, invocation);
3770
3963
  const run = state.activeRuns.get(sessionId);
3771
- if (run) this.cancelRun(run, reason);
3964
+ if (run) this.cancelRun(run, reason, invocation as IFlexAgentContextInvocation<TScope>);
3965
+ if (run) {
3966
+ const settled = await Promise.allSettled([run.completion]);
3967
+ if (settled[0].status === 'rejected') {
3968
+ this.appendUnexpectedErrors(errors, settled[0].reason);
3969
+ }
3970
+ }
3772
3971
  if (!retained.stored.agentSessionAbortCompleted) {
3773
3972
  try {
3774
- await this.abortStoredSession(retained.stored, reason);
3973
+ await this.abortStoredSession(retained.stored, reason, invocation);
3775
3974
  } catch (error) {
3776
3975
  errors.push(this.projectOperationError(
3777
3976
  error,
@@ -3782,20 +3981,16 @@ export class FlexHarness<TScope = unknown> {
3782
3981
  ));
3783
3982
  }
3784
3983
  }
3785
- if (run) {
3786
- const settled = await Promise.allSettled([run.completion]);
3787
- if (settled[0].status === 'rejected' && !isAbortError(settled[0].reason)) {
3788
- errors.push(settled[0].reason);
3789
- }
3790
- }
3791
3984
  if (retained.stored.promptQueueDrain) {
3792
3985
  const settled = await Promise.allSettled([retained.stored.promptQueueDrain]);
3793
- if (settled[0].status === 'rejected') errors.push(settled[0].reason);
3986
+ if (settled[0].status === 'rejected') {
3987
+ this.appendUnexpectedErrors(errors, settled[0].reason);
3988
+ }
3794
3989
  }
3795
3990
  try {
3796
- await this.closeStoredSession(retained.stored);
3991
+ await this.closeStoredSession(retained.stored, invocation);
3797
3992
  } catch (error) {
3798
- errors.push(this.projectOperationError(
3993
+ this.appendUnexpectedErrors(errors, this.projectOperationError(
3799
3994
  error,
3800
3995
  'toolCleanup',
3801
3996
  scopeId,
@@ -3803,7 +3998,12 @@ export class FlexHarness<TScope = unknown> {
3803
3998
  'session-delete',
3804
3999
  ));
3805
4000
  }
3806
- if (errors.length > 0) throw combineErrors(errors);
4001
+ if (errors.length > 0) {
4002
+ if (state.lifecycle === 'retired' && !this.storedSessionCleanupCompleted(retained.stored)) {
4003
+ this.orphanedStoredSessions.add(retained.stored);
4004
+ }
4005
+ throw combineErrors(errors);
4006
+ }
3807
4007
  this.purgeStoredPromptQueue(retained.stored);
3808
4008
  }
3809
4009
  if (!retained?.domainsCompleted) {
@@ -3829,17 +4029,33 @@ export class FlexHarness<TScope = unknown> {
3829
4029
  state.lifecycle = 'fenced';
3830
4030
  const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
3831
4031
  const reason = this.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.'));
4032
+ if (!state.compactorLifecycleController.signal.aborted) {
4033
+ state.compactorLifecycleController.abort(reason);
4034
+ }
3832
4035
  const cleanupErrors: unknown[] = [];
4036
+ const ambientContext = this.compactorInvocationContext.getStore();
4037
+ const invocation = ambientContext?.storageKey === state.storageKey
4038
+ && ambientContext.sessionId === currentRun.sessionId
4039
+ ? ambientContext
4040
+ : this.effectiveRunCompactorContext(currentRun);
4041
+ const contextFor = (sessionId: string) => this.createCompactorContext(
4042
+ invocation.scopeId,
4043
+ invocation.scope,
4044
+ state.storageKey,
4045
+ sessionId,
4046
+ );
3833
4047
  try {
3834
4048
  for (const stored of state.sessions.values()) {
4049
+ this.abortCompactorLifecycle(stored, reason);
3835
4050
  this.cancelStoredPromptQueue(
3836
4051
  stored,
3837
4052
  reason,
3838
4053
  stored === currentRun.stored ? currentRun.queueId : undefined,
4054
+ contextFor(stored.session.sessionId),
3839
4055
  );
3840
4056
  }
3841
4057
  const otherRuns = [...state.activeRuns.values()].filter((run) => run !== currentRun);
3842
- for (const run of otherRuns) this.cancelRun(run, reason);
4058
+ for (const run of otherRuns) this.cancelRun(run, reason, contextFor(run.sessionId));
3843
4059
  const runResults = await Promise.allSettled(otherRuns.map((run) => run.completion));
3844
4060
  for (const result of runResults) {
3845
4061
  if (result.status === 'rejected') this.appendUnexpectedErrors(cleanupErrors, result.reason);
@@ -3847,7 +4063,16 @@ export class FlexHarness<TScope = unknown> {
3847
4063
  await Promise.allSettled([...state.sessionInitializations.values()]);
3848
4064
  cleanupErrors.push(...await this.closeOrphanedResources(state.storageKey));
3849
4065
  await state.scopeQueue;
3850
- const tombstoneAttempts = [...state.tombstoneCleanups.entries()];
4066
+ const currentTombstoneCleanup = state.tombstoneCleanups.get(currentRun.sessionId);
4067
+ if (currentTombstoneCleanup) {
4068
+ this.retainOrphanedTombstoneCleanup(
4069
+ state.storageKey,
4070
+ currentRun.sessionId,
4071
+ currentTombstoneCleanup,
4072
+ );
4073
+ }
4074
+ const tombstoneAttempts = [...state.tombstoneCleanups.entries()]
4075
+ .filter(([sessionId]) => sessionId !== currentRun.sessionId);
3851
4076
  const tombstoneResults = await Promise.allSettled(
3852
4077
  tombstoneAttempts.map(([, cleanup]) => cleanup),
3853
4078
  );
@@ -3858,12 +4083,13 @@ export class FlexHarness<TScope = unknown> {
3858
4083
  const storedSessions = new Set([
3859
4084
  ...state.sessions.values(),
3860
4085
  ...[...state.retainedSessionCleanups]
3861
- .filter(([sessionId]) => !attemptedTombstones.has(sessionId))
4086
+ .filter(([sessionId]) =>
4087
+ sessionId !== currentRun.sessionId && !attemptedTombstones.has(sessionId))
3862
4088
  .map(([, retained]) => retained.stored),
3863
4089
  ]);
3864
4090
  for (const stored of storedSessions) {
3865
4091
  try {
3866
- await this.abortStoredSession(stored, reason);
4092
+ await this.abortStoredSession(stored, reason, contextFor(stored.session.sessionId));
3867
4093
  } catch (error) {
3868
4094
  cleanupErrors.push(this.projectOperationError(
3869
4095
  error,
@@ -3874,7 +4100,7 @@ export class FlexHarness<TScope = unknown> {
3874
4100
  ));
3875
4101
  }
3876
4102
  try {
3877
- await this.closeStoredSession(stored);
4103
+ await this.closeStoredSession(stored, contextFor(stored.session.sessionId));
3878
4104
  } catch (error) {
3879
4105
  if (!this.storedSessionCleanupCompleted(stored)) {
3880
4106
  cleanupErrors.push(this.projectOperationError(
@@ -3893,24 +4119,41 @@ export class FlexHarness<TScope = unknown> {
3893
4119
  if (cleanupErrors.length > 0) throw combineErrors([cause, ...cleanupErrors]);
3894
4120
  state.lifecycle = 'retired';
3895
4121
  state.sessions.clear();
4122
+ const currentRetainedCleanup = state.retainedSessionCleanups.get(currentRun.sessionId);
3896
4123
  state.retainedSessionCleanups.clear();
4124
+ if (currentRetainedCleanup) {
4125
+ state.retainedSessionCleanups.set(currentRun.sessionId, currentRetainedCleanup);
4126
+ }
3897
4127
  state.sessionDeletions.clear();
3898
4128
  state.tombstoneCleanups.clear();
4129
+ if (currentTombstoneCleanup) {
4130
+ state.tombstoneCleanups.set(currentRun.sessionId, currentTombstoneCleanup);
4131
+ }
3899
4132
  state.activeRuns.clear();
3900
4133
  state.pendingPermissions.clear();
3901
4134
  state.initializingSessions.clear();
3902
4135
  state.sessionInitializations.clear();
3903
4136
  this.stateLoads.delete(state.storageKey);
4137
+ if (
4138
+ this.storageCompactorLifecycleControllers.get(state.storageKey)
4139
+ === state.compactorLifecycleController
4140
+ ) this.storageCompactorLifecycleControllers.delete(state.storageKey);
3904
4141
  } finally {
3905
4142
  state.fenceInProgress = false;
3906
4143
  }
3907
4144
  }
3908
4145
 
3909
- private async closeStoredSession(stored: IStoredSessionState): Promise<void> {
4146
+ private async closeStoredSession(
4147
+ stored: IStoredSessionState,
4148
+ context: IFlexAgentContextInvocation<unknown> = stored.compactorContext!,
4149
+ ): Promise<void> {
3910
4150
  const errors: unknown[] = [];
3911
4151
  if (!stored.agentSessionCloseCompleted) {
3912
4152
  try {
3913
- await stored.agentSession.close();
4153
+ await this.withCompactorContext(
4154
+ context,
4155
+ () => stored.agentSession.close(),
4156
+ );
3914
4157
  if (!stored.agentSession.closeCleanupCompleted) {
3915
4158
  throw new Error('AgentSession.close() resolved before cleanup ownership was released.');
3916
4159
  }
@@ -3960,9 +4203,13 @@ export class FlexHarness<TScope = unknown> {
3960
4203
  private async abortStoredSession(
3961
4204
  stored: IStoredSessionState,
3962
4205
  reason: FlexHarnessAbortError,
4206
+ context: IFlexAgentContextInvocation<unknown> = stored.compactorContext!,
3963
4207
  ): Promise<void> {
3964
4208
  if (stored.agentSessionAbortCompleted) return;
3965
- await stored.agentSession.abortSession(reason, { abortBackgroundJobs: true });
4209
+ await this.withCompactorContext(
4210
+ context,
4211
+ () => stored.agentSession.abortSession(reason, { abortBackgroundJobs: true }),
4212
+ );
3966
4213
  stored.agentSessionAbortCompleted = true;
3967
4214
  }
3968
4215
 
@@ -4018,6 +4265,29 @@ export class FlexHarness<TScope = unknown> {
4018
4265
  retained.storageKey === storageKey && retained.sessionId === sessionId);
4019
4266
  }
4020
4267
 
4268
+ private retainOrphanedTombstoneCleanup(
4269
+ storageKey: string,
4270
+ sessionId: string,
4271
+ completion: Promise<void>,
4272
+ ): void {
4273
+ const key = JSON.stringify([storageKey, sessionId]);
4274
+ if (this.orphanedTombstoneCleanups.has(key)) return;
4275
+ const retained: IOrphanedTombstoneCleanup = { storageKey, sessionId, completion };
4276
+ this.orphanedTombstoneCleanups.set(key, retained);
4277
+ void completion.then(
4278
+ () => {
4279
+ if (this.orphanedTombstoneCleanups.get(key) === retained) {
4280
+ this.orphanedTombstoneCleanups.delete(key);
4281
+ }
4282
+ },
4283
+ () => {
4284
+ if (this.orphanedTombstoneCleanups.get(key) === retained) {
4285
+ this.orphanedTombstoneCleanups.delete(key);
4286
+ }
4287
+ },
4288
+ );
4289
+ }
4290
+
4021
4291
  private closeOrphanedResources(storageKey?: string): Promise<unknown[]> {
4022
4292
  const operation = this.orphanedResourceQueue.then(() =>
4023
4293
  this.closeOrphanedResourcesInternal(storageKey));
@@ -4027,6 +4297,14 @@ export class FlexHarness<TScope = unknown> {
4027
4297
 
4028
4298
  private async closeOrphanedResourcesInternal(storageKey?: string): Promise<unknown[]> {
4029
4299
  const errors: unknown[] = [];
4300
+ const tombstoneCleanups = [...this.orphanedTombstoneCleanups.values()]
4301
+ .filter((retained) => storageKey === undefined || retained.storageKey === storageKey);
4302
+ const tombstoneResults = await Promise.allSettled(
4303
+ tombstoneCleanups.map((retained) => retained.completion),
4304
+ );
4305
+ for (const result of tombstoneResults) {
4306
+ if (result.status === 'rejected') this.appendUnexpectedErrors(errors, result.reason);
4307
+ }
4030
4308
  for (const stored of [...this.orphanedStoredSessions]) {
4031
4309
  if (storageKey !== undefined && stored.storageKey !== storageKey) continue;
4032
4310
  try {
@@ -4067,6 +4345,7 @@ export class FlexHarness<TScope = unknown> {
4067
4345
  ...[...this.orphanedStoredSessions].map((stored) => stored.storageKey),
4068
4346
  ...[...this.orphanedExecutionContexts.values()].map((owner) => owner.storageKey),
4069
4347
  ...[...this.orphanedProviderReleases.values()].map((retained) => retained.storageKey),
4348
+ ...[...this.orphanedTombstoneCleanups.values()].map((retained) => retained.storageKey),
4070
4349
  ]);
4071
4350
  }
4072
4351
 
@@ -4089,7 +4368,12 @@ export class FlexHarness<TScope = unknown> {
4089
4368
  }
4090
4369
  } else {
4091
4370
  try {
4092
- await this.drainStorage(scope.storageKey, stateLoad, this.createScopeRetirementError());
4371
+ await this.drainStorage(
4372
+ scope.storageKey,
4373
+ stateLoad,
4374
+ this.createScopeRetirementError(),
4375
+ { scopeId, scope: scope.scope },
4376
+ );
4093
4377
  } catch (error) {
4094
4378
  this.appendUnexpectedErrors(errors, error);
4095
4379
  }
@@ -4101,15 +4385,25 @@ export class FlexHarness<TScope = unknown> {
4101
4385
  storageKey: string,
4102
4386
  stateLoad: Promise<IStorageState>,
4103
4387
  reason: FlexHarnessAbortError,
4388
+ invocation?: { scopeId: string; scope: TScope },
4104
4389
  ): Promise<void> {
4390
+ const compactorController = this.storageCompactorLifecycleControllers.get(storageKey);
4391
+ if (compactorController && !compactorController.signal.aborted) {
4392
+ compactorController.abort(reason);
4393
+ }
4105
4394
  const existing = this.storageDrains.get(storageKey);
4106
4395
  if (existing) return existing;
4107
4396
  let completed = false;
4108
4397
  let drain!: Promise<void>;
4109
- drain = this.drainStorageInternal(storageKey, stateLoad, reason).then(() => {
4398
+ drain = this.drainStorageInternal(storageKey, stateLoad, reason, invocation).then(() => {
4110
4399
  completed = true;
4111
4400
  }).finally(() => {
4112
- if (completed && this.stateLoads.get(storageKey) === stateLoad) this.stateLoads.delete(storageKey);
4401
+ if (completed && this.stateLoads.get(storageKey) === stateLoad) {
4402
+ this.stateLoads.delete(storageKey);
4403
+ if (this.storageCompactorLifecycleControllers.get(storageKey) === compactorController) {
4404
+ this.storageCompactorLifecycleControllers.delete(storageKey);
4405
+ }
4406
+ }
4113
4407
  if (this.storageDrains.get(storageKey) === drain) this.storageDrains.delete(storageKey);
4114
4408
  });
4115
4409
  this.storageDrains.set(storageKey, drain);
@@ -4120,6 +4414,7 @@ export class FlexHarness<TScope = unknown> {
4120
4414
  storageKey: string,
4121
4415
  stateLoad: Promise<IStorageState>,
4122
4416
  reason: FlexHarnessAbortError,
4417
+ invocation?: { scopeId: string; scope: TScope },
4123
4418
  ): Promise<void> {
4124
4419
  let state: IStorageState;
4125
4420
  try {
@@ -4132,14 +4427,35 @@ export class FlexHarness<TScope = unknown> {
4132
4427
  if (state.lifecycle !== 'fenced') state.lifecycle = 'retiring';
4133
4428
  const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
4134
4429
  const errors: unknown[] = [];
4135
- for (const stored of state.sessions.values()) this.cancelStoredPromptQueue(stored, reason);
4430
+ const contextFor = (sessionId: string) => invocation
4431
+ ? this.createCompactorContext(invocation.scopeId, invocation.scope, storageKey, sessionId)
4432
+ : undefined;
4433
+ for (const stored of state.sessions.values()) {
4434
+ this.abortCompactorLifecycle(stored, reason);
4435
+ this.cancelStoredPromptQueue(
4436
+ stored,
4437
+ reason,
4438
+ undefined,
4439
+ contextFor(stored.session.sessionId),
4440
+ );
4441
+ }
4136
4442
  for (const run of state.activeRuns.values()) {
4137
- if (run.phase !== 'finalizing' && run.phase !== 'promoting') this.cancelRun(run, reason);
4443
+ if (run.phase !== 'finalizing' && run.phase !== 'promoting') {
4444
+ this.cancelRun(run, reason, contextFor(run.sessionId));
4445
+ }
4138
4446
  }
4139
4447
  for (const pending of [...state.pendingPermissions.values()]) this.rejectPending(state, pending, reason);
4448
+ const runResults = await Promise.allSettled([...state.activeRuns.values()].map((run) => run.completion));
4449
+ for (const result of runResults) {
4450
+ if (result.status === 'rejected') this.appendUnexpectedErrors(errors, result.reason);
4451
+ }
4140
4452
  for (const stored of state.sessions.values()) {
4141
4453
  try {
4142
- await this.abortStoredSession(stored, reason);
4454
+ await this.abortStoredSession(
4455
+ stored,
4456
+ reason,
4457
+ contextFor(stored.session.sessionId),
4458
+ );
4143
4459
  } catch (error) {
4144
4460
  errors.push(this.projectOperationError(
4145
4461
  error,
@@ -4150,10 +4466,6 @@ export class FlexHarness<TScope = unknown> {
4150
4466
  ));
4151
4467
  }
4152
4468
  }
4153
- const runResults = await Promise.allSettled([...state.activeRuns.values()].map((run) => run.completion));
4154
- for (const result of runResults) {
4155
- if (result.status === 'rejected') this.appendUnexpectedErrors(errors, result.reason);
4156
- }
4157
4469
  await Promise.allSettled([...state.sessionInitializations.values()]);
4158
4470
  errors.push(...await this.closeOrphanedResources(state.storageKey));
4159
4471
  await state.scopeQueue;
@@ -4173,7 +4485,7 @@ export class FlexHarness<TScope = unknown> {
4173
4485
  errors.push(...await this.settleDetachedCleanups(state, detachedCleanupAttempts));
4174
4486
  for (const stored of state.sessions.values()) {
4175
4487
  try {
4176
- await this.closeStoredSession(stored);
4488
+ await this.closeStoredSession(stored, contextFor(stored.session.sessionId));
4177
4489
  } catch (error) {
4178
4490
  this.appendUnexpectedErrors(errors, this.projectOperationError(
4179
4491
  error,
@@ -4192,7 +4504,8 @@ export class FlexHarness<TScope = unknown> {
4192
4504
  await this.finishTombstoneCleanup(
4193
4505
  state,
4194
4506
  sessionId,
4195
- retained?.stored.session.scopeId ?? state.scopeIdHint,
4507
+ invocation?.scopeId ?? retained?.stored.session.scopeId ?? state.scopeIdHint,
4508
+ invocation?.scope,
4196
4509
  );
4197
4510
  } catch (error) {
4198
4511
  this.appendUnexpectedErrors(errors, error);
@@ -4240,10 +4553,12 @@ export class FlexHarness<TScope = unknown> {
4240
4553
  }
4241
4554
  this.listeners.clear();
4242
4555
  if (errors.length > 0) throw combineErrors(errors);
4556
+ this.compactorInvocationContext.disable();
4243
4557
  this.stateLoads.clear();
4244
4558
  this.scopeAdmissions.clear();
4245
4559
  this.scopeRetirements.clear();
4246
4560
  this.storageDrains.clear();
4561
+ this.storageCompactorLifecycleControllers.clear();
4247
4562
  }
4248
4563
 
4249
4564
  private appendUnexpectedErrors(target: unknown[], error: unknown): void {
@@ -4252,6 +4567,12 @@ export class FlexHarness<TScope = unknown> {
4252
4567
  } else if (!isAbortError(error)) target.push(error);
4253
4568
  }
4254
4569
 
4570
+ private abortCompactorLifecycle(stored: IStoredSessionState, reason: FlexHarnessAbortError): void {
4571
+ if (!stored.compactorLifecycleController.signal.aborted) {
4572
+ stored.compactorLifecycleController.abort(reason);
4573
+ }
4574
+ }
4575
+
4255
4576
  private async resolveState(
4256
4577
  scopeId: string,
4257
4578
  ): Promise<{ scope: IFlexResolvedScope<TScope>; state: IStorageState }> {
@@ -4279,10 +4600,26 @@ export class FlexHarness<TScope = unknown> {
4279
4600
  ) throw this.createScopeRetirementError();
4280
4601
  stateLoad = this.stateLoads.get(scope.storageKey);
4281
4602
  if (!stateLoad) {
4282
- stateLoad = this.loadState(scope.storageKey, scopeId, scope.scope);
4603
+ const compactorLifecycleController = new AbortController();
4604
+ this.storageCompactorLifecycleControllers.set(
4605
+ scope.storageKey,
4606
+ compactorLifecycleController,
4607
+ );
4608
+ stateLoad = this.loadState(
4609
+ scope.storageKey,
4610
+ scopeId,
4611
+ scope.scope,
4612
+ compactorLifecycleController,
4613
+ );
4283
4614
  this.stateLoads.set(scope.storageKey, stateLoad);
4284
4615
  void stateLoad.catch(() => {
4285
- if (this.stateLoads.get(scope.storageKey) === stateLoad) this.stateLoads.delete(scope.storageKey);
4616
+ if (this.stateLoads.get(scope.storageKey) === stateLoad) {
4617
+ this.stateLoads.delete(scope.storageKey);
4618
+ if (
4619
+ this.storageCompactorLifecycleControllers.get(scope.storageKey)
4620
+ === compactorLifecycleController
4621
+ ) this.storageCompactorLifecycleControllers.delete(scope.storageKey);
4622
+ }
4286
4623
  });
4287
4624
  }
4288
4625
  }
@@ -4328,6 +4665,37 @@ export class FlexHarness<TScope = unknown> {
4328
4665
  }
4329
4666
  }
4330
4667
 
4668
+ private createCompactorContext(
4669
+ scopeId: string,
4670
+ scope: TScope,
4671
+ storageKey: string,
4672
+ sessionId: string,
4673
+ ): IFlexAgentContextInvocation<TScope> {
4674
+ return Object.freeze({ scopeId, scope, storageKey, sessionId });
4675
+ }
4676
+
4677
+ private withCompactorContext<TResult>(
4678
+ context: IFlexAgentContextInvocation<unknown>,
4679
+ operation: () => TResult,
4680
+ ): TResult {
4681
+ return this.compactorInvocationContext.run(
4682
+ context as IFlexAgentContextInvocation<TScope>,
4683
+ operation,
4684
+ );
4685
+ }
4686
+
4687
+ private createRunCompactorContext(run: IActiveRun): IFlexAgentContextInvocation<TScope> {
4688
+ return run.originCompactorContext as IFlexAgentContextInvocation<TScope>;
4689
+ }
4690
+
4691
+ private effectiveRunCompactorContext(run: IActiveRun): IFlexAgentContextInvocation<TScope> {
4692
+ return (run.deferredCompactorContext ?? run.originCompactorContext) as IFlexAgentContextInvocation<TScope>;
4693
+ }
4694
+
4695
+ private withRunCompactorContext<TResult>(run: IActiveRun, operation: () => TResult): TResult {
4696
+ return this.withCompactorContext(this.effectiveRunCompactorContext(run), operation);
4697
+ }
4698
+
4331
4699
  private requireSession(state: IStorageState, sessionId: string): IStoredSessionState {
4332
4700
  validateIdentifier(sessionId, 'sessionId');
4333
4701
  requireTransferIdentifier(sessionId, 'sessionId');