@modelprofile.com/flexharness 3.2.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.
@@ -932,7 +961,9 @@ export class FlexHarness<TScope = unknown> {
932
961
  const { state } = await this.resolveState(scopeId);
933
962
  let result!: IFlexSession;
934
963
  await this.mutateScope(state, () => {
935
- const stored = this.requireMutableSession(state, sessionId);
964
+ const stored = Object.prototype.hasOwnProperty.call(options, 'archived')
965
+ ? this.requireMutableSession(state, sessionId)
966
+ : this.requireSession(state, sessionId);
936
967
  const timestamp = new Date().toISOString();
937
968
  if (Object.prototype.hasOwnProperty.call(options, 'title')) {
938
969
  if (options.title === null) delete stored.session.title;
@@ -948,12 +979,12 @@ export class FlexHarness<TScope = unknown> {
948
979
  }
949
980
 
950
981
  public async deleteSession(scopeId: string, sessionId: string): Promise<void> {
951
- const { state } = await this.resolveState(scopeId);
982
+ const { scope, state } = await this.resolveState(scopeId);
952
983
  validateIdentifier(sessionId, 'sessionId');
953
984
  const existingDeletion = state.sessionDeletions.get(sessionId);
954
985
  if (existingDeletion) return existingDeletion;
955
986
  let deletion!: Promise<void>;
956
- deletion = this.deleteSessionInternal(state, scopeId, sessionId).finally(() => {
987
+ deletion = this.deleteSessionInternal(state, scopeId, scope.scope, sessionId).finally(() => {
957
988
  if (state.sessionDeletions.get(sessionId) === deletion) {
958
989
  state.sessionDeletions.delete(sessionId);
959
990
  }
@@ -965,12 +996,13 @@ export class FlexHarness<TScope = unknown> {
965
996
  private async deleteSessionInternal(
966
997
  state: IStorageState,
967
998
  scopeId: string,
999
+ scope: TScope,
968
1000
  sessionId: string,
969
1001
  ): Promise<void> {
970
1002
  const existingTombstone = state.tombstones.get(sessionId);
971
1003
  if (existingTombstone) {
972
1004
  try {
973
- await this.finishTombstoneCleanup(state, sessionId, scopeId);
1005
+ await this.finishTombstoneCleanup(state, sessionId, scopeId, scope);
974
1006
  } catch (error) {
975
1007
  throw this.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-delete');
976
1008
  }
@@ -993,7 +1025,7 @@ export class FlexHarness<TScope = unknown> {
993
1025
  });
994
1026
  });
995
1027
  try {
996
- await this.finishTombstoneCleanup(state, sessionId, scopeId);
1028
+ await this.finishTombstoneCleanup(state, sessionId, scopeId, scope);
997
1029
  } catch (error) {
998
1030
  throw this.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-delete');
999
1031
  }
@@ -1175,7 +1207,7 @@ export class FlexHarness<TScope = unknown> {
1175
1207
  ): Promise<boolean> {
1176
1208
  validateIdentifier(queueId, 'queueId');
1177
1209
  requireTransferIdentifier(queueId, 'queueId');
1178
- const { state } = await this.resolveState(scopeId);
1210
+ const { scope, state } = await this.resolveState(scopeId);
1179
1211
  const stored = this.requireSession(state, sessionId);
1180
1212
  const queued = stored.outstandingPromptsById.get(queueId);
1181
1213
  if (!queued) {
@@ -1185,6 +1217,7 @@ export class FlexHarness<TScope = unknown> {
1185
1217
  return this.cancelQueuedPrompt(
1186
1218
  queued,
1187
1219
  this.trustInternalError(new FlexHarnessAbortError('The queued prompt was cancelled.')),
1220
+ this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
1188
1221
  );
1189
1222
  }
1190
1223
 
@@ -1194,20 +1227,28 @@ export class FlexHarness<TScope = unknown> {
1194
1227
  scheduleKey: string,
1195
1228
  ): Promise<boolean> {
1196
1229
  validateIdentifier(scheduleKey, 'scheduleKey');
1197
- const { state } = await this.resolveState(scopeId);
1230
+ const { scope, state } = await this.resolveState(scopeId);
1198
1231
  const stored = this.requireSession(state, sessionId);
1199
1232
  const queued = [...stored.outstandingPromptsById.values()]
1200
1233
  .find((entry) => entry.scheduleKey === scheduleKey);
1201
1234
  if (!queued) return false;
1202
1235
  const cancellation = this.trustInternalError(new FlexHarnessAbortError('The scheduled run was cancelled.'));
1203
- return this.cancelQueuedPrompt(queued, cancellation);
1236
+ return this.cancelQueuedPrompt(
1237
+ queued,
1238
+ cancellation,
1239
+ this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
1240
+ );
1204
1241
  }
1205
1242
 
1206
1243
  public async abort(scopeId: string, sessionId: string): Promise<boolean> {
1207
- const { state } = await this.resolveState(scopeId);
1244
+ const { scope, state } = await this.resolveState(scopeId);
1208
1245
  const run = state.activeRuns.get(sessionId);
1209
1246
  if (!run || run.phase === 'finalizing' || run.phase === 'promoting') return false;
1210
- 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
+ );
1211
1252
  return true;
1212
1253
  }
1213
1254
 
@@ -1231,12 +1272,16 @@ export class FlexHarness<TScope = unknown> {
1231
1272
  decision: TFlexPermissionDecision,
1232
1273
  ): Promise<void> {
1233
1274
  validateIdentifier(permissionId, 'permissionId');
1234
- const { state } = await this.resolveState(scopeId);
1275
+ const { scope, state } = await this.resolveState(scopeId);
1235
1276
  const pending = state.pendingPermissions.get(permissionId);
1236
1277
  if (!pending || pending.request.sessionId !== sessionId) {
1237
1278
  throw new FlexHarnessNotFoundError('Permission', permissionId);
1238
1279
  }
1239
- 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
+ ));
1240
1285
  pending.responseQueue = response.then(() => undefined, () => undefined);
1241
1286
  return response;
1242
1287
  }
@@ -1273,7 +1318,7 @@ export class FlexHarness<TScope = unknown> {
1273
1318
  if (!reconciliation || typeof reconciliation !== 'object' || Array.isArray(reconciliation)) {
1274
1319
  throw new FlexHarnessValidationError('Tool execution reconciliation must be a plain object.');
1275
1320
  }
1276
- const { state } = await this.resolveState(scopeId);
1321
+ const { scope, state } = await this.resolveState(scopeId);
1277
1322
  const stored = this.requireSession(state, sessionId);
1278
1323
  this.assertStateAcceptingWork(state);
1279
1324
  let canonical: plugins.TAgentToolExecutionReconciliationOptions;
@@ -1297,7 +1342,10 @@ export class FlexHarness<TScope = unknown> {
1297
1342
  throw new FlexHarnessValidationError('Unknown tool execution reconciliation.');
1298
1343
  }
1299
1344
  try {
1300
- 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
+ );
1301
1349
  } catch (error) {
1302
1350
  throw this.projectOperationError(error, 'agentSession', scopeId, sessionId, 'tool-reconciliation');
1303
1351
  }
@@ -1309,22 +1357,28 @@ export class FlexHarness<TScope = unknown> {
1309
1357
  event: IJsonObject & { type: string },
1310
1358
  ): Promise<void> {
1311
1359
  this.validateRuntimeEvent(event);
1312
- const { state } = await this.resolveState(scopeId);
1360
+ const { scope, state } = await this.resolveState(scopeId);
1313
1361
  const stored = this.requireSession(state, sessionId);
1314
1362
  this.assertStateAcceptingWork(state);
1315
1363
  try {
1316
- 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
+ );
1317
1368
  } catch (error) {
1318
1369
  throw this.projectOperationError(error, 'agentSession', scopeId, sessionId, 'runtime-event');
1319
1370
  }
1320
1371
  }
1321
1372
 
1322
1373
  public async compactSession(scopeId: string, sessionId: string): Promise<void> {
1323
- const { state } = await this.resolveState(scopeId);
1374
+ const { scope, state } = await this.resolveState(scopeId);
1324
1375
  const stored = this.requireSession(state, sessionId);
1325
1376
  this.assertStateAcceptingWork(state);
1326
1377
  try {
1327
- await stored.agentSession.compact();
1378
+ await this.withCompactorContext(
1379
+ this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
1380
+ () => stored.agentSession.compact(),
1381
+ );
1328
1382
  } catch (error) {
1329
1383
  throw this.projectOperationError(error, 'agentSession', scopeId, sessionId, 'compaction');
1330
1384
  }
@@ -1401,11 +1455,14 @@ export class FlexHarness<TScope = unknown> {
1401
1455
  ): Promise<void> {
1402
1456
  validateIdentifier(executionId, 'executionId');
1403
1457
  requireTransferIdentifier(executionId, 'executionId');
1404
- const { state } = await this.resolveState(scopeId);
1458
+ const { scope, state } = await this.resolveState(scopeId);
1405
1459
  const stored = this.requireSession(state, sessionId);
1406
1460
  this.assertStateAcceptingWork(state);
1407
1461
  try {
1408
- await stored.agentSession.abortBackgroundExecution(executionId);
1462
+ await this.withCompactorContext(
1463
+ this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
1464
+ () => stored.agentSession.abortBackgroundExecution(executionId),
1465
+ );
1409
1466
  } catch (error) {
1410
1467
  throw this.projectOperationError(error, 'agentSession', scopeId, sessionId, 'background-abort');
1411
1468
  }
@@ -1634,6 +1691,12 @@ export class FlexHarness<TScope = unknown> {
1634
1691
  return {
1635
1692
  state: queued.state,
1636
1693
  stored: queued.stored,
1694
+ originCompactorContext: this.createCompactorContext(
1695
+ queued.scopeId,
1696
+ queued.scope as TScope,
1697
+ queued.state.storageKey,
1698
+ queued.sessionId,
1699
+ ),
1637
1700
  scopeId: queued.scopeId,
1638
1701
  scope: queued.scope,
1639
1702
  sessionId: queued.sessionId,
@@ -1674,11 +1737,14 @@ export class FlexHarness<TScope = unknown> {
1674
1737
  const options = queued.options!;
1675
1738
  let projectionReserved = false;
1676
1739
  try {
1677
- run.transaction = await run.stored.agentSession.beginGeneration(
1678
- cloneSerializable(prompt.modelMessage.content) as Parameters<
1679
- plugins.IAgentSession['beginGeneration']
1680
- >[0],
1681
- { 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
+ ),
1682
1748
  );
1683
1749
  this.assertPromptPromotion(queued, run);
1684
1750
  const reservation = this.createReservation(run, prompt);
@@ -1757,13 +1823,13 @@ export class FlexHarness<TScope = unknown> {
1757
1823
  prepare: (context: { generationId: string; abortSignal: AbortSignal }) =>
1758
1824
  this.prepareGeneration(run, options, context.abortSignal),
1759
1825
  };
1760
- const generation = run.scheduleKey
1826
+ const generation = this.withRunCompactorContext(run, () => run.scheduleKey
1761
1827
  ? run.stored.agentSession.scheduleGenerate({
1762
1828
  key: run.scheduleKey,
1763
1829
  debounceMs: run.debounceMs,
1764
1830
  ...generateOptions,
1765
1831
  })
1766
- : run.stored.agentSession.generate(generateOptions);
1832
+ : run.stored.agentSession.generate(generateOptions));
1767
1833
  const execution = this.executeRun(run, generation).then(
1768
1834
  (result) => this.finishQueuedPrompt(queued, 'completed', undefined, result),
1769
1835
  (error) => this.finishQueuedPrompt(
@@ -1851,7 +1917,11 @@ export class FlexHarness<TScope = unknown> {
1851
1917
  }
1852
1918
  }
1853
1919
 
1854
- private cancelQueuedPrompt(queued: IQueuedPrompt, reason: FlexHarnessAbortError): boolean {
1920
+ private cancelQueuedPrompt(
1921
+ queued: IQueuedPrompt,
1922
+ reason: FlexHarnessAbortError,
1923
+ context?: IFlexAgentContextInvocation<TScope>,
1924
+ ): boolean {
1855
1925
  if (queued.stored.outstandingPromptsById.get(queued.queueId) !== queued) return false;
1856
1926
  if (queued.status === 'queued') {
1857
1927
  this.finishQueuedPrompt(queued, 'cancelled', reason);
@@ -1861,7 +1931,7 @@ export class FlexHarness<TScope = unknown> {
1861
1931
  const run = queued.state.activeRuns.get(queued.sessionId);
1862
1932
  if (!run || run.queueId !== queued.queueId) return false;
1863
1933
  if (run.phase === 'finalizing' || run.phase === 'promoting') return false;
1864
- this.cancelRun(run, reason);
1934
+ this.cancelRun(run, reason, context);
1865
1935
  return true;
1866
1936
  }
1867
1937
 
@@ -1869,10 +1939,15 @@ export class FlexHarness<TScope = unknown> {
1869
1939
  stored: IStoredSessionState,
1870
1940
  reason: FlexHarnessAbortError,
1871
1941
  excludedQueueId?: string,
1942
+ context?: IFlexAgentContextInvocation<unknown>,
1872
1943
  ): void {
1873
1944
  for (const queued of [...stored.outstandingPromptsById.values()]) {
1874
1945
  if (queued.queueId === excludedQueueId) continue;
1875
- this.cancelQueuedPrompt(queued, reason);
1946
+ this.cancelQueuedPrompt(
1947
+ queued,
1948
+ reason,
1949
+ context as IFlexAgentContextInvocation<TScope> | undefined,
1950
+ );
1876
1951
  }
1877
1952
  }
1878
1953
 
@@ -1906,7 +1981,10 @@ export class FlexHarness<TScope = unknown> {
1906
1981
  if (run.controller.signal.aborted) throw run.controller.signal.reason;
1907
1982
  run.phase = 'finalizing';
1908
1983
  try {
1909
- await run.stored.agentSession.finalizeGeneration(run.transaction!, 'accepted');
1984
+ await this.withRunCompactorContext(
1985
+ run,
1986
+ () => run.stored.agentSession.finalizeGeneration(run.transaction!, 'accepted'),
1987
+ );
1910
1988
  } catch (error) {
1911
1989
  const projected = this.projectExternalError(run, error, 'agentSession');
1912
1990
  await this.fenceNamespace(run.state, run, projected);
@@ -1976,7 +2054,10 @@ export class FlexHarness<TScope = unknown> {
1976
2054
  run.phase = 'finalizing';
1977
2055
  const desiredOutcome: TCanonicalOutcome = cancelled || !generated ? 'interrupted' : 'rejected';
1978
2056
  try {
1979
- await run.stored.agentSession.finalizeGeneration(run.transaction!, desiredOutcome);
2057
+ await this.withRunCompactorContext(
2058
+ run,
2059
+ () => run.stored.agentSession.finalizeGeneration(run.transaction!, desiredOutcome),
2060
+ );
1980
2061
  } catch (finalizationError) {
1981
2062
  errors.push(this.projectExternalError(run, finalizationError, 'agentSession'));
1982
2063
  }
@@ -2350,11 +2431,14 @@ export class FlexHarness<TScope = unknown> {
2350
2431
  private emitTerminalProjection(run: IActiveRun, terminal: IFlexTerminalProjection): void {
2351
2432
  for (const part of terminal.assistantMessage.parts) {
2352
2433
  const callbackPart = run.callbackParts.find((entry) => entry.partId === part.partId);
2353
- if (
2354
- (part.type === 'reasoning' || part.type === 'tool')
2355
- && callbackPart?.type === part.type
2356
- && callbackPart.status === 'running'
2357
- ) {
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) {
2358
2442
  this.emitPartEvent(run, 'part.completed', part);
2359
2443
  }
2360
2444
  }
@@ -2615,6 +2699,7 @@ export class FlexHarness<TScope = unknown> {
2615
2699
  state: IStorageState,
2616
2700
  pending: IPendingPermission,
2617
2701
  decision: TFlexPermissionDecision,
2702
+ context: IFlexAgentContextInvocation<TScope>,
2618
2703
  ): Promise<void> {
2619
2704
  if (pending.settled) {
2620
2705
  throw new FlexHarnessPermissionStateError(`Permission "${pending.request.permissionId}" has already been resolved.`);
@@ -2712,7 +2797,7 @@ export class FlexHarness<TScope = unknown> {
2712
2797
  const rejection = this.trustInternalError(
2713
2798
  new FlexHarnessPermissionRejectedError(pending.request.permissionId),
2714
2799
  );
2715
- this.abortRunInternally(pending.run, rejection);
2800
+ this.abortRunInternally(pending.run, rejection, context);
2716
2801
  pending.reject(rejection);
2717
2802
  } else {
2718
2803
  pending.resolve();
@@ -2816,19 +2901,50 @@ export class FlexHarness<TScope = unknown> {
2816
2901
  pending.reject(error);
2817
2902
  }
2818
2903
 
2819
- private cancelRun(run: IActiveRun, reason: FlexHarnessAbortError): void {
2820
- if (run.internalFailure === undefined) run.ownerCancellation ??= reason;
2821
- this.rejectRunPermissions(run.state, run, reason);
2822
- if (!run.controller.signal.aborted) run.controller.abort(reason);
2823
- if (run.phase === 'admitting') return;
2824
- if (run.scheduleKey) run.stored.agentSession.cancelScheduledGeneration(run.scheduleKey, reason);
2825
- 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
+ );
2826
2928
  }
2827
2929
 
2828
- private abortRunInternally(run: IActiveRun, error: unknown): void {
2829
- run.internalFailure ??= error ?? new Error('Internal FlexHarness run failure.');
2830
- if (!run.controller.signal.aborted) run.controller.abort(run.internalFailure);
2831
- 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
+ );
2832
2948
  }
2833
2949
 
2834
2950
  private async rollbackAdmission(
@@ -2839,7 +2955,10 @@ export class FlexHarness<TScope = unknown> {
2839
2955
  const errors: unknown[] = [];
2840
2956
  if (run.transaction) {
2841
2957
  try {
2842
- await run.stored.agentSession.finalizeGeneration(run.transaction, 'interrupted');
2958
+ await this.withRunCompactorContext(
2959
+ run,
2960
+ () => run.stored.agentSession.finalizeGeneration(run.transaction!, 'interrupted'),
2961
+ );
2843
2962
  } catch (error) {
2844
2963
  errors.push(this.projectExternalError(run, error, 'agentSession'));
2845
2964
  }
@@ -3066,6 +3185,7 @@ export class FlexHarness<TScope = unknown> {
3066
3185
  storageKey: string,
3067
3186
  scopeId: string,
3068
3187
  scope: TScope,
3188
+ compactorLifecycleController: AbortController,
3069
3189
  ): Promise<IStorageState> {
3070
3190
  const snapshot = cloneSerializable(await this.stores.scopes.load(storageKey) ?? {
3071
3191
  schemaVersion: 1,
@@ -3075,6 +3195,7 @@ export class FlexHarness<TScope = unknown> {
3075
3195
  } satisfies IFlexScopeSnapshot);
3076
3196
  const state: IStorageState = {
3077
3197
  storageKey,
3198
+ compactorLifecycleController,
3078
3199
  scopeIdHint: scopeId,
3079
3200
  revision: snapshot.revision,
3080
3201
  sessions: new Map(),
@@ -3162,6 +3283,7 @@ export class FlexHarness<TScope = unknown> {
3162
3283
  agentEventStoreReleased: true,
3163
3284
  executionContextCloseCompleted: true,
3164
3285
  jobStoreReleased: true,
3286
+ compactorLifecycleController: new AbortController(),
3165
3287
  promptQueue: [],
3166
3288
  outstandingPromptsById: new Map(),
3167
3289
  terminalPromptQueueEntries: new Map(),
@@ -3176,6 +3298,12 @@ export class FlexHarness<TScope = unknown> {
3176
3298
  scope: TScope,
3177
3299
  ): Promise<IStoredSessionState> {
3178
3300
  const sessionId = metadata.sessionId;
3301
+ const compactorContext = this.createCompactorContext(
3302
+ scopeId,
3303
+ scope,
3304
+ state.storageKey,
3305
+ sessionId,
3306
+ );
3179
3307
  const [projectionResult, permissionResult, eventStoreResult, jobStoreResult] = await Promise.allSettled([
3180
3308
  this.stores.projections.load(state.storageKey, sessionId),
3181
3309
  this.stores.permissions.load(state.storageKey, sessionId),
@@ -3214,13 +3342,16 @@ export class FlexHarness<TScope = unknown> {
3214
3342
  let executionContextHandle: IFlexExecutionContextHandle | undefined;
3215
3343
  try {
3216
3344
  if (this.executionContextProvider) {
3217
- executionContextHandle = await this.executionContextProvider.provideExecutionContext({
3218
- scopeId,
3219
- scope,
3220
- storageKey: state.storageKey,
3221
- sessionId,
3222
- jobStore,
3223
- });
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
+ );
3224
3355
  if (
3225
3356
  executionContextHandle
3226
3357
  && (!executionContextHandle.context
@@ -3231,6 +3362,7 @@ export class FlexHarness<TScope = unknown> {
3231
3362
  }
3232
3363
  const stored: IStoredSessionState = {
3233
3364
  storageKey: state.storageKey,
3365
+ compactorContext,
3234
3366
  session: cloneSerializable(metadata),
3235
3367
  messages: cloneSerializable(projection?.messages ?? []),
3236
3368
  stagedTerminals: cloneSerializable(projection?.stagedTerminals ?? []),
@@ -3248,21 +3380,75 @@ export class FlexHarness<TScope = unknown> {
3248
3380
  executionContextCloseCompleted: executionContextHandle?.close === undefined,
3249
3381
  jobStoreReleased: this.stores.jobs.releaseSession === undefined,
3250
3382
  jobs: executionContextHandle?.context.jobs,
3383
+ compactorLifecycleController: new AbortController(),
3251
3384
  promptQueue: [],
3252
3385
  outstandingPromptsById: new Map(),
3253
3386
  terminalPromptQueueEntries: new Map(),
3254
3387
  outstandingPromptBytes: 0,
3255
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;
3256
3409
  const agentSessionOptions: plugins.IAgentSessionOptions & {
3257
3410
  transactionOutcomeErrorProjector: (error: unknown) => string;
3258
3411
  } = {
3259
- ...this.agentSessionPolicy,
3412
+ ...agentSessionPolicy,
3260
3413
  sessionId,
3261
3414
  eventStore,
3262
- executionContext: executionContextHandle?.context,
3415
+ executionContext: contextualExecutionContext,
3263
3416
  contextBuilder: ({ events }) => hydrateAgentMessages(
3264
3417
  (this.agentSessionPolicy.contextBuilder ?? ((options) => plugins.buildModelMessages(options.events)))({ events }),
3265
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
+ : {}),
3266
3452
  transactionOutcomeErrorProjector: () => 'The model operation failed.',
3267
3453
  onToken: (delta) => this.onTextDelta(state, sessionId, delta),
3268
3454
  onReasoningStart: (id) => this.onReasoningStart(state, sessionId, id),
@@ -3271,7 +3457,10 @@ export class FlexHarness<TScope = unknown> {
3271
3457
  onToolCallStart: (event) => this.onToolStart(state, sessionId, event),
3272
3458
  onToolCallFinish: (event) => this.onToolFinish(state, sessionId, event),
3273
3459
  };
3274
- stored.agentSession = await plugins.AgentSession.create(agentSessionOptions);
3460
+ stored.agentSession = await this.withCompactorContext(
3461
+ compactorContext,
3462
+ () => plugins.AgentSession.create(agentSessionOptions),
3463
+ );
3275
3464
  return stored;
3276
3465
  } catch (error) {
3277
3466
  const cleanupErrors: unknown[] = [];
@@ -3742,11 +3931,12 @@ export class FlexHarness<TScope = unknown> {
3742
3931
  state: IStorageState,
3743
3932
  sessionId: string,
3744
3933
  scopeId: string,
3934
+ scope?: TScope,
3745
3935
  ): Promise<void> {
3746
3936
  const existing = state.tombstoneCleanups.get(sessionId);
3747
3937
  if (existing) return existing;
3748
3938
  let cleanup!: Promise<void>;
3749
- cleanup = this.finishTombstoneCleanupInternal(state, sessionId, scopeId).finally(() => {
3939
+ cleanup = this.finishTombstoneCleanupInternal(state, sessionId, scopeId, scope).finally(() => {
3750
3940
  if (state.tombstoneCleanups.get(sessionId) === cleanup) {
3751
3941
  state.tombstoneCleanups.delete(sessionId);
3752
3942
  }
@@ -3759,17 +3949,28 @@ export class FlexHarness<TScope = unknown> {
3759
3949
  state: IStorageState,
3760
3950
  sessionId: string,
3761
3951
  scopeId: string,
3952
+ scope?: TScope,
3762
3953
  ): Promise<void> {
3763
3954
  const retained = state.retainedSessionCleanups.get(sessionId);
3764
3955
  if (retained) {
3765
3956
  const errors: unknown[] = [];
3766
3957
  const reason = this.trustInternalError(new FlexHarnessAbortError('The session was deleted.'));
3767
- 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);
3768
3963
  const run = state.activeRuns.get(sessionId);
3769
- 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
+ }
3770
3971
  if (!retained.stored.agentSessionAbortCompleted) {
3771
3972
  try {
3772
- await this.abortStoredSession(retained.stored, reason);
3973
+ await this.abortStoredSession(retained.stored, reason, invocation);
3773
3974
  } catch (error) {
3774
3975
  errors.push(this.projectOperationError(
3775
3976
  error,
@@ -3780,20 +3981,16 @@ export class FlexHarness<TScope = unknown> {
3780
3981
  ));
3781
3982
  }
3782
3983
  }
3783
- if (run) {
3784
- const settled = await Promise.allSettled([run.completion]);
3785
- if (settled[0].status === 'rejected' && !isAbortError(settled[0].reason)) {
3786
- errors.push(settled[0].reason);
3787
- }
3788
- }
3789
3984
  if (retained.stored.promptQueueDrain) {
3790
3985
  const settled = await Promise.allSettled([retained.stored.promptQueueDrain]);
3791
- if (settled[0].status === 'rejected') errors.push(settled[0].reason);
3986
+ if (settled[0].status === 'rejected') {
3987
+ this.appendUnexpectedErrors(errors, settled[0].reason);
3988
+ }
3792
3989
  }
3793
3990
  try {
3794
- await this.closeStoredSession(retained.stored);
3991
+ await this.closeStoredSession(retained.stored, invocation);
3795
3992
  } catch (error) {
3796
- errors.push(this.projectOperationError(
3993
+ this.appendUnexpectedErrors(errors, this.projectOperationError(
3797
3994
  error,
3798
3995
  'toolCleanup',
3799
3996
  scopeId,
@@ -3801,7 +3998,12 @@ export class FlexHarness<TScope = unknown> {
3801
3998
  'session-delete',
3802
3999
  ));
3803
4000
  }
3804
- 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
+ }
3805
4007
  this.purgeStoredPromptQueue(retained.stored);
3806
4008
  }
3807
4009
  if (!retained?.domainsCompleted) {
@@ -3827,17 +4029,33 @@ export class FlexHarness<TScope = unknown> {
3827
4029
  state.lifecycle = 'fenced';
3828
4030
  const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
3829
4031
  const reason = this.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.'));
4032
+ if (!state.compactorLifecycleController.signal.aborted) {
4033
+ state.compactorLifecycleController.abort(reason);
4034
+ }
3830
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
+ );
3831
4047
  try {
3832
4048
  for (const stored of state.sessions.values()) {
4049
+ this.abortCompactorLifecycle(stored, reason);
3833
4050
  this.cancelStoredPromptQueue(
3834
4051
  stored,
3835
4052
  reason,
3836
4053
  stored === currentRun.stored ? currentRun.queueId : undefined,
4054
+ contextFor(stored.session.sessionId),
3837
4055
  );
3838
4056
  }
3839
4057
  const otherRuns = [...state.activeRuns.values()].filter((run) => run !== currentRun);
3840
- for (const run of otherRuns) this.cancelRun(run, reason);
4058
+ for (const run of otherRuns) this.cancelRun(run, reason, contextFor(run.sessionId));
3841
4059
  const runResults = await Promise.allSettled(otherRuns.map((run) => run.completion));
3842
4060
  for (const result of runResults) {
3843
4061
  if (result.status === 'rejected') this.appendUnexpectedErrors(cleanupErrors, result.reason);
@@ -3845,7 +4063,16 @@ export class FlexHarness<TScope = unknown> {
3845
4063
  await Promise.allSettled([...state.sessionInitializations.values()]);
3846
4064
  cleanupErrors.push(...await this.closeOrphanedResources(state.storageKey));
3847
4065
  await state.scopeQueue;
3848
- 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);
3849
4076
  const tombstoneResults = await Promise.allSettled(
3850
4077
  tombstoneAttempts.map(([, cleanup]) => cleanup),
3851
4078
  );
@@ -3856,12 +4083,13 @@ export class FlexHarness<TScope = unknown> {
3856
4083
  const storedSessions = new Set([
3857
4084
  ...state.sessions.values(),
3858
4085
  ...[...state.retainedSessionCleanups]
3859
- .filter(([sessionId]) => !attemptedTombstones.has(sessionId))
4086
+ .filter(([sessionId]) =>
4087
+ sessionId !== currentRun.sessionId && !attemptedTombstones.has(sessionId))
3860
4088
  .map(([, retained]) => retained.stored),
3861
4089
  ]);
3862
4090
  for (const stored of storedSessions) {
3863
4091
  try {
3864
- await this.abortStoredSession(stored, reason);
4092
+ await this.abortStoredSession(stored, reason, contextFor(stored.session.sessionId));
3865
4093
  } catch (error) {
3866
4094
  cleanupErrors.push(this.projectOperationError(
3867
4095
  error,
@@ -3872,7 +4100,7 @@ export class FlexHarness<TScope = unknown> {
3872
4100
  ));
3873
4101
  }
3874
4102
  try {
3875
- await this.closeStoredSession(stored);
4103
+ await this.closeStoredSession(stored, contextFor(stored.session.sessionId));
3876
4104
  } catch (error) {
3877
4105
  if (!this.storedSessionCleanupCompleted(stored)) {
3878
4106
  cleanupErrors.push(this.projectOperationError(
@@ -3891,24 +4119,41 @@ export class FlexHarness<TScope = unknown> {
3891
4119
  if (cleanupErrors.length > 0) throw combineErrors([cause, ...cleanupErrors]);
3892
4120
  state.lifecycle = 'retired';
3893
4121
  state.sessions.clear();
4122
+ const currentRetainedCleanup = state.retainedSessionCleanups.get(currentRun.sessionId);
3894
4123
  state.retainedSessionCleanups.clear();
4124
+ if (currentRetainedCleanup) {
4125
+ state.retainedSessionCleanups.set(currentRun.sessionId, currentRetainedCleanup);
4126
+ }
3895
4127
  state.sessionDeletions.clear();
3896
4128
  state.tombstoneCleanups.clear();
4129
+ if (currentTombstoneCleanup) {
4130
+ state.tombstoneCleanups.set(currentRun.sessionId, currentTombstoneCleanup);
4131
+ }
3897
4132
  state.activeRuns.clear();
3898
4133
  state.pendingPermissions.clear();
3899
4134
  state.initializingSessions.clear();
3900
4135
  state.sessionInitializations.clear();
3901
4136
  this.stateLoads.delete(state.storageKey);
4137
+ if (
4138
+ this.storageCompactorLifecycleControllers.get(state.storageKey)
4139
+ === state.compactorLifecycleController
4140
+ ) this.storageCompactorLifecycleControllers.delete(state.storageKey);
3902
4141
  } finally {
3903
4142
  state.fenceInProgress = false;
3904
4143
  }
3905
4144
  }
3906
4145
 
3907
- private async closeStoredSession(stored: IStoredSessionState): Promise<void> {
4146
+ private async closeStoredSession(
4147
+ stored: IStoredSessionState,
4148
+ context: IFlexAgentContextInvocation<unknown> = stored.compactorContext!,
4149
+ ): Promise<void> {
3908
4150
  const errors: unknown[] = [];
3909
4151
  if (!stored.agentSessionCloseCompleted) {
3910
4152
  try {
3911
- await stored.agentSession.close();
4153
+ await this.withCompactorContext(
4154
+ context,
4155
+ () => stored.agentSession.close(),
4156
+ );
3912
4157
  if (!stored.agentSession.closeCleanupCompleted) {
3913
4158
  throw new Error('AgentSession.close() resolved before cleanup ownership was released.');
3914
4159
  }
@@ -3958,9 +4203,13 @@ export class FlexHarness<TScope = unknown> {
3958
4203
  private async abortStoredSession(
3959
4204
  stored: IStoredSessionState,
3960
4205
  reason: FlexHarnessAbortError,
4206
+ context: IFlexAgentContextInvocation<unknown> = stored.compactorContext!,
3961
4207
  ): Promise<void> {
3962
4208
  if (stored.agentSessionAbortCompleted) return;
3963
- await stored.agentSession.abortSession(reason, { abortBackgroundJobs: true });
4209
+ await this.withCompactorContext(
4210
+ context,
4211
+ () => stored.agentSession.abortSession(reason, { abortBackgroundJobs: true }),
4212
+ );
3964
4213
  stored.agentSessionAbortCompleted = true;
3965
4214
  }
3966
4215
 
@@ -4016,6 +4265,29 @@ export class FlexHarness<TScope = unknown> {
4016
4265
  retained.storageKey === storageKey && retained.sessionId === sessionId);
4017
4266
  }
4018
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
+
4019
4291
  private closeOrphanedResources(storageKey?: string): Promise<unknown[]> {
4020
4292
  const operation = this.orphanedResourceQueue.then(() =>
4021
4293
  this.closeOrphanedResourcesInternal(storageKey));
@@ -4025,6 +4297,14 @@ export class FlexHarness<TScope = unknown> {
4025
4297
 
4026
4298
  private async closeOrphanedResourcesInternal(storageKey?: string): Promise<unknown[]> {
4027
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
+ }
4028
4308
  for (const stored of [...this.orphanedStoredSessions]) {
4029
4309
  if (storageKey !== undefined && stored.storageKey !== storageKey) continue;
4030
4310
  try {
@@ -4065,6 +4345,7 @@ export class FlexHarness<TScope = unknown> {
4065
4345
  ...[...this.orphanedStoredSessions].map((stored) => stored.storageKey),
4066
4346
  ...[...this.orphanedExecutionContexts.values()].map((owner) => owner.storageKey),
4067
4347
  ...[...this.orphanedProviderReleases.values()].map((retained) => retained.storageKey),
4348
+ ...[...this.orphanedTombstoneCleanups.values()].map((retained) => retained.storageKey),
4068
4349
  ]);
4069
4350
  }
4070
4351
 
@@ -4087,7 +4368,12 @@ export class FlexHarness<TScope = unknown> {
4087
4368
  }
4088
4369
  } else {
4089
4370
  try {
4090
- 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
+ );
4091
4377
  } catch (error) {
4092
4378
  this.appendUnexpectedErrors(errors, error);
4093
4379
  }
@@ -4099,15 +4385,25 @@ export class FlexHarness<TScope = unknown> {
4099
4385
  storageKey: string,
4100
4386
  stateLoad: Promise<IStorageState>,
4101
4387
  reason: FlexHarnessAbortError,
4388
+ invocation?: { scopeId: string; scope: TScope },
4102
4389
  ): Promise<void> {
4390
+ const compactorController = this.storageCompactorLifecycleControllers.get(storageKey);
4391
+ if (compactorController && !compactorController.signal.aborted) {
4392
+ compactorController.abort(reason);
4393
+ }
4103
4394
  const existing = this.storageDrains.get(storageKey);
4104
4395
  if (existing) return existing;
4105
4396
  let completed = false;
4106
4397
  let drain!: Promise<void>;
4107
- drain = this.drainStorageInternal(storageKey, stateLoad, reason).then(() => {
4398
+ drain = this.drainStorageInternal(storageKey, stateLoad, reason, invocation).then(() => {
4108
4399
  completed = true;
4109
4400
  }).finally(() => {
4110
- 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
+ }
4111
4407
  if (this.storageDrains.get(storageKey) === drain) this.storageDrains.delete(storageKey);
4112
4408
  });
4113
4409
  this.storageDrains.set(storageKey, drain);
@@ -4118,6 +4414,7 @@ export class FlexHarness<TScope = unknown> {
4118
4414
  storageKey: string,
4119
4415
  stateLoad: Promise<IStorageState>,
4120
4416
  reason: FlexHarnessAbortError,
4417
+ invocation?: { scopeId: string; scope: TScope },
4121
4418
  ): Promise<void> {
4122
4419
  let state: IStorageState;
4123
4420
  try {
@@ -4130,14 +4427,35 @@ export class FlexHarness<TScope = unknown> {
4130
4427
  if (state.lifecycle !== 'fenced') state.lifecycle = 'retiring';
4131
4428
  const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
4132
4429
  const errors: unknown[] = [];
4133
- 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
+ }
4134
4442
  for (const run of state.activeRuns.values()) {
4135
- 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
+ }
4136
4446
  }
4137
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
+ }
4138
4452
  for (const stored of state.sessions.values()) {
4139
4453
  try {
4140
- await this.abortStoredSession(stored, reason);
4454
+ await this.abortStoredSession(
4455
+ stored,
4456
+ reason,
4457
+ contextFor(stored.session.sessionId),
4458
+ );
4141
4459
  } catch (error) {
4142
4460
  errors.push(this.projectOperationError(
4143
4461
  error,
@@ -4148,10 +4466,6 @@ export class FlexHarness<TScope = unknown> {
4148
4466
  ));
4149
4467
  }
4150
4468
  }
4151
- const runResults = await Promise.allSettled([...state.activeRuns.values()].map((run) => run.completion));
4152
- for (const result of runResults) {
4153
- if (result.status === 'rejected') this.appendUnexpectedErrors(errors, result.reason);
4154
- }
4155
4469
  await Promise.allSettled([...state.sessionInitializations.values()]);
4156
4470
  errors.push(...await this.closeOrphanedResources(state.storageKey));
4157
4471
  await state.scopeQueue;
@@ -4171,7 +4485,7 @@ export class FlexHarness<TScope = unknown> {
4171
4485
  errors.push(...await this.settleDetachedCleanups(state, detachedCleanupAttempts));
4172
4486
  for (const stored of state.sessions.values()) {
4173
4487
  try {
4174
- await this.closeStoredSession(stored);
4488
+ await this.closeStoredSession(stored, contextFor(stored.session.sessionId));
4175
4489
  } catch (error) {
4176
4490
  this.appendUnexpectedErrors(errors, this.projectOperationError(
4177
4491
  error,
@@ -4190,7 +4504,8 @@ export class FlexHarness<TScope = unknown> {
4190
4504
  await this.finishTombstoneCleanup(
4191
4505
  state,
4192
4506
  sessionId,
4193
- retained?.stored.session.scopeId ?? state.scopeIdHint,
4507
+ invocation?.scopeId ?? retained?.stored.session.scopeId ?? state.scopeIdHint,
4508
+ invocation?.scope,
4194
4509
  );
4195
4510
  } catch (error) {
4196
4511
  this.appendUnexpectedErrors(errors, error);
@@ -4238,10 +4553,12 @@ export class FlexHarness<TScope = unknown> {
4238
4553
  }
4239
4554
  this.listeners.clear();
4240
4555
  if (errors.length > 0) throw combineErrors(errors);
4556
+ this.compactorInvocationContext.disable();
4241
4557
  this.stateLoads.clear();
4242
4558
  this.scopeAdmissions.clear();
4243
4559
  this.scopeRetirements.clear();
4244
4560
  this.storageDrains.clear();
4561
+ this.storageCompactorLifecycleControllers.clear();
4245
4562
  }
4246
4563
 
4247
4564
  private appendUnexpectedErrors(target: unknown[], error: unknown): void {
@@ -4250,6 +4567,12 @@ export class FlexHarness<TScope = unknown> {
4250
4567
  } else if (!isAbortError(error)) target.push(error);
4251
4568
  }
4252
4569
 
4570
+ private abortCompactorLifecycle(stored: IStoredSessionState, reason: FlexHarnessAbortError): void {
4571
+ if (!stored.compactorLifecycleController.signal.aborted) {
4572
+ stored.compactorLifecycleController.abort(reason);
4573
+ }
4574
+ }
4575
+
4253
4576
  private async resolveState(
4254
4577
  scopeId: string,
4255
4578
  ): Promise<{ scope: IFlexResolvedScope<TScope>; state: IStorageState }> {
@@ -4277,10 +4600,26 @@ export class FlexHarness<TScope = unknown> {
4277
4600
  ) throw this.createScopeRetirementError();
4278
4601
  stateLoad = this.stateLoads.get(scope.storageKey);
4279
4602
  if (!stateLoad) {
4280
- 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
+ );
4281
4614
  this.stateLoads.set(scope.storageKey, stateLoad);
4282
4615
  void stateLoad.catch(() => {
4283
- 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
+ }
4284
4623
  });
4285
4624
  }
4286
4625
  }
@@ -4326,6 +4665,37 @@ export class FlexHarness<TScope = unknown> {
4326
4665
  }
4327
4666
  }
4328
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
+
4329
4699
  private requireSession(state: IStorageState, sessionId: string): IStoredSessionState {
4330
4700
  validateIdentifier(sessionId, 'sessionId');
4331
4701
  requireTransferIdentifier(sessionId, 'sessionId');