@modelprofile.com/flexharness 5.1.0 → 5.3.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.
@@ -33,6 +33,10 @@ import type {
33
33
  IFlexCallbackLimits,
34
34
  IFlexCreateProjectTaskInput,
35
35
  IFlexCreateSessionOptions,
36
+ IFlexDeleteSessionGenerationCohortInput,
37
+ IFlexDeleteSessionGenerationCohortResult,
38
+ IFlexDelegatedRunAdmissionContext,
39
+ IFlexDelegatedRunAdmissionLease,
36
40
  IFlexErrorInfo,
37
41
  IFlexEventArchiveMetadata,
38
42
  IFlexExecutionContextHandle,
@@ -75,6 +79,7 @@ import type {
75
79
  IFlexScopeSnapshot,
76
80
  IFlexSession,
77
81
  IFlexSessionGeneration,
82
+ IFlexSessionGenerationCohortEntry,
78
83
  IFlexSessionReversionGroup,
79
84
  IFlexSessionReversionInfo,
80
85
  IFlexSessionTombstone,
@@ -174,11 +179,13 @@ import {
174
179
  import { wrapToolSet } from './utils.tooloutput.js';
175
180
  import {
176
181
  validateCreateProjectTaskInput,
182
+ validateDeleteSessionGenerationCohortInput,
177
183
  validateIdentifier,
178
184
  validateProjectTaskId,
179
185
  validateProjectedErrorInfo,
180
186
  validatePromptOptions,
181
187
  validateSlashCommandExecutionOptions,
188
+ validateSessionGenerationId,
182
189
  validateUpdateProjectTaskInput,
183
190
  validateUpdateSessionOptions,
184
191
  validateUtf8String,
@@ -292,6 +299,7 @@ interface IStorageState {
292
299
  tombstones: Map<string, IFlexSessionTombstone>;
293
300
  tombstoneCleanups: Map<string, Promise<void>>;
294
301
  activeRuns: Map<string, IActiveRun>;
302
+ delegatedRunAdmissionOwners: Set<IDelegatedRunAdmissionOwner>;
295
303
  pendingPermissions: Map<string, IPendingPermission>;
296
304
  scopeQueue: Promise<void>;
297
305
  lifecycle: 'active' | 'retiring' | 'fenced' | 'retired';
@@ -326,6 +334,7 @@ interface IActiveRun {
326
334
  callbacksClosed: boolean;
327
335
  reasoningPartIds: Map<string, string>;
328
336
  toolPartIds: Map<string, string>;
337
+ trustedToolExecutionErrors: Map<string, Error>;
329
338
  subagentCallCount: number;
330
339
  subagentSessionIds: Set<string>;
331
340
  pendingPermissionIds: Set<string>;
@@ -333,9 +342,56 @@ interface IActiveRun {
333
342
  reservedUserMessage?: IFlexMessage;
334
343
  reservedAssistantMessage?: IFlexMessage;
335
344
  modelResolution?: IFlexResolvedModel;
345
+ delegatedRunAdmissionOwner?: IDelegatedRunAdmissionOwner;
346
+ delegatedRunAdmissionClose?: Promise<void>;
336
347
  completion: Promise<IFlexPromptResult>;
337
348
  }
338
349
 
350
+ interface IDelegatedRunAdmissionSeed {
351
+ readonly state: IStorageState;
352
+ readonly childStored: IStoredSessionState;
353
+ readonly parentRun: IActiveRun;
354
+ readonly parentStored: IStoredSessionState;
355
+ readonly scopeId: string;
356
+ readonly scope: unknown;
357
+ readonly storageKey: string;
358
+ readonly sessionId: string;
359
+ readonly sessionGenerationId: string;
360
+ readonly sessionGenerationSequence: number;
361
+ readonly originParentRunId: string;
362
+ readonly originParentToolCallId: string;
363
+ readonly parentSessionId: string;
364
+ readonly parentSessionGenerationId: string;
365
+ readonly parentSessionGenerationSequence: number;
366
+ readonly parentQueueId: string;
367
+ readonly parentRunId: string;
368
+ readonly parentToolCallId: string;
369
+ readonly agent: string;
370
+ readonly depth: number;
371
+ }
372
+
373
+ type TDelegatedRunAdmissionOwnerStatus =
374
+ | 'acquiring'
375
+ | 'acquired'
376
+ | 'close-requested'
377
+ | 'retrying'
378
+ | 'closed';
379
+
380
+ interface IDelegatedRunAdmissionOwner {
381
+ readonly storageState: IStorageState;
382
+ readonly run: IActiveRun;
383
+ readonly context: Readonly<IFlexDelegatedRunAdmissionContext<unknown>>;
384
+ status: TDelegatedRunAdmissionOwnerStatus;
385
+ acquisitionSettled: boolean;
386
+ acquisitionDetached: boolean;
387
+ acquisitionCompletion?: Promise<void>;
388
+ acquisitionError?: Error;
389
+ lease?: IFlexDelegatedRunAdmissionLease;
390
+ closeAttempt?: Promise<void>;
391
+ closeAttemptSequence: number;
392
+ closeFailure?: { sequence: number; error: Error };
393
+ }
394
+
339
395
  interface IQueuedPrompt {
340
396
  state: IStorageState;
341
397
  stored: IStoredSessionState;
@@ -352,6 +408,7 @@ interface IQueuedPrompt {
352
408
  startedAt?: string;
353
409
  prompt?: INormalizedFlexPrompt;
354
410
  options?: IFlexPromptOptions;
411
+ delegationSeed?: IDelegatedRunAdmissionSeed;
355
412
  byteSize: number;
356
413
  completion: Promise<IFlexPromptResult>;
357
414
  resolveCompletion: (result: IFlexPromptResult) => void;
@@ -439,6 +496,16 @@ interface IFlexSubagentAcquisition {
439
496
  created: boolean;
440
497
  }
441
498
 
499
+ interface ISessionDeletionPlan {
500
+ matched: boolean;
501
+ deletion?: Promise<void>;
502
+ }
503
+
504
+ interface IReservedSessionDeletion {
505
+ deletedSessions: IFlexSession[];
506
+ descendantRoots: string[];
507
+ }
508
+
442
509
  type TProjectTaskToolInput =
443
510
  | { action: 'list' }
444
511
  | {
@@ -534,6 +601,18 @@ function requireSessionGeneration(
534
601
  };
535
602
  }
536
603
 
604
+ function matchesSessionGeneration(
605
+ session: Pick<
606
+ IFlexSession | IFlexSessionTombstone,
607
+ 'sessionGenerationId' | 'sessionGenerationSequence'
608
+ >,
609
+ expected: Readonly<IFlexSessionGeneration>,
610
+ ): boolean {
611
+ const actual = requireSessionGeneration(session);
612
+ return actual.sessionGenerationId === expected.sessionGenerationId
613
+ && actual.sessionGenerationSequence === expected.sessionGenerationSequence;
614
+ }
615
+
537
616
  function sha256Hex(value: string): string {
538
617
  return plugins.crypto.createHash('sha256').update(value, 'utf8').digest('hex');
539
618
  }
@@ -603,6 +682,7 @@ export class FlexHarness<TScope = unknown> {
603
682
  private readonly toolProvider: IFlexHarnessOptions<TScope>['toolProvider'];
604
683
  private readonly resourceToolProviderResolver: IFlexHarnessOptions<TScope>['resourceToolProviderResolver'];
605
684
  private readonly executionContextProvider: IFlexHarnessOptions<TScope>['executionContextProvider'];
685
+ private readonly delegatedRunAdmissionProvider: IFlexHarnessOptions<TScope>['delegatedRunAdmissionProvider'];
606
686
  private readonly stores: IFlexHarnessStores;
607
687
  private readonly agentSessionPolicy: IFlexAgentSessionPolicy<TScope>;
608
688
  private readonly toolOutputLimits: Required<NonNullable<IFlexHarnessOptions<TScope>['toolOutputLimits']>>;
@@ -666,6 +746,7 @@ export class FlexHarness<TScope = unknown> {
666
746
  this.toolProvider = options.toolProvider;
667
747
  this.resourceToolProviderResolver = options.resourceToolProviderResolver;
668
748
  this.executionContextProvider = options.executionContextProvider;
749
+ this.delegatedRunAdmissionProvider = options.delegatedRunAdmissionProvider;
669
750
  this.stores = requireHarnessStores(options.stores ?? new InMemoryFlexHarnessStores());
670
751
  this.agentSessionPolicy = normalizeAgentSessionPolicy(options.agentSessionPolicy);
671
752
  this.toolOutputLimits = resolveJsonLimits(options.toolOutputLimits);
@@ -727,8 +808,14 @@ export class FlexHarness<TScope = unknown> {
727
808
  if (!options || typeof options !== 'object' || Array.isArray(options)) {
728
809
  throw new FlexHarnessValidationError('createSession options must be a plain object.');
729
810
  }
730
- const unsupported = Object.keys(options).find((key) => key !== 'sessionId' && key !== 'title');
811
+ const unsupported = Object.keys(options).find(
812
+ (key) => !['sessionId', 'sessionGenerationId', 'title'].includes(key),
813
+ );
731
814
  if (unsupported) throw new FlexHarnessValidationError(`createSession does not support "${unsupported}".`);
815
+ const requestedSessionGenerationId = options.sessionGenerationId;
816
+ if (requestedSessionGenerationId !== undefined) {
817
+ validateSessionGenerationId(requestedSessionGenerationId);
818
+ }
732
819
  const resolved = await this.resolveState(scopeId);
733
820
  const sessionId = options.sessionId ?? plugins.crypto.randomUUID();
734
821
  validateIdentifier(sessionId, 'sessionId');
@@ -775,7 +862,7 @@ export class FlexHarness<TScope = unknown> {
775
862
  metadata = {
776
863
  scopeId,
777
864
  sessionId,
778
- sessionGenerationId: createSessionGenerationId(),
865
+ sessionGenerationId: requestedSessionGenerationId ?? createSessionGenerationId(),
779
866
  sessionGenerationSequence: resolved.state.revision + 1,
780
867
  ...(options.title ? { title: options.title } : {}),
781
868
  createdAt: timestamp,
@@ -878,7 +965,7 @@ export class FlexHarness<TScope = unknown> {
878
965
  completeInitialization();
879
966
  }
880
967
  const result = publicSnapshot(metadata);
881
- this.emitEvent(scopeId, sessionId, { type: 'session.created', session: result });
968
+ this.emitEvent(scopeId, metadata, { type: 'session.created', session: result });
882
969
  return result;
883
970
  }
884
971
 
@@ -946,7 +1033,7 @@ export class FlexHarness<TScope = unknown> {
946
1033
  stored.session.updatedAt = timestamp;
947
1034
  result = publicSnapshot(stored.session);
948
1035
  });
949
- this.emitEvent(scopeId, sessionId, { type: 'session.updated', session: result });
1036
+ this.emitEvent(scopeId, result, { type: 'session.updated', session: result });
950
1037
  return result;
951
1038
  }
952
1039
 
@@ -1125,17 +1212,136 @@ export class FlexHarness<TScope = unknown> {
1125
1212
  );
1126
1213
  }
1127
1214
 
1215
+ public async deleteSessionGenerationCohort(
1216
+ scopeId: string,
1217
+ input: IFlexDeleteSessionGenerationCohortInput,
1218
+ ): Promise<IFlexDeleteSessionGenerationCohortResult> {
1219
+ const validated = validateDeleteSessionGenerationCohortInput(input);
1220
+ requireTransferIdentifier(validated.root.sessionId, 'root.sessionId');
1221
+ for (const entry of validated.authorizedBySessionId.values()) {
1222
+ requireTransferIdentifier(entry.sessionId, 'authorizedCohort sessionId');
1223
+ }
1224
+ this.assertNoAmbientSlashCommandTeardown({
1225
+ scopeId,
1226
+ sessionId: validated.root.sessionId,
1227
+ });
1228
+ const { scope, state } = await this.resolveState(scopeId);
1229
+ this.assertNoAmbientSlashCommandTeardown({ storageKey: state.storageKey });
1230
+ const plan = await this.withScopeMutationOwnership(state, async (): Promise<ISessionDeletionPlan> => {
1231
+ const stored = state.sessions.get(validated.root.sessionId);
1232
+ if (stored) {
1233
+ if (!matchesSessionGeneration(stored.session, validated.root)) {
1234
+ return { matched: false };
1235
+ }
1236
+ const subtree = this.collectSessionSubtree(state, validated.root.sessionId);
1237
+ const descendantRoots = this.descendantTombstoneRoots(
1238
+ state,
1239
+ validated.root.sessionId,
1240
+ );
1241
+ this.assertAuthorizedSessionGenerationCohort(
1242
+ [
1243
+ ...subtree.map((entry) => entry.session),
1244
+ ...this.tombstonesForRoots(state, descendantRoots),
1245
+ ],
1246
+ validated.authorizedBySessionId,
1247
+ );
1248
+ const reservation = await this.mutateScopeOwned(state, () =>
1249
+ this.reserveLiveSessionDeletion(
1250
+ state,
1251
+ validated.root.sessionId,
1252
+ stored,
1253
+ subtree,
1254
+ descendantRoots,
1255
+ ));
1256
+ return {
1257
+ matched: true,
1258
+ deletion: this.startReservedSessionDeletion(
1259
+ state,
1260
+ scopeId,
1261
+ scope.scope,
1262
+ validated.root.sessionId,
1263
+ reservation,
1264
+ ),
1265
+ };
1266
+ }
1267
+ const tombstone = state.tombstones.get(validated.root.sessionId);
1268
+ if (!tombstone || !matchesSessionGeneration(tombstone, validated.root)) {
1269
+ return { matched: false };
1270
+ }
1271
+ const rootSessionId = tombstone.rootSessionId ?? tombstone.sessionId;
1272
+ const descendantRoots = this.descendantTombstoneRoots(state, rootSessionId);
1273
+ this.assertAuthorizedSessionGenerationCohort(
1274
+ this.tombstonesForRoots(state, [rootSessionId, ...descendantRoots]),
1275
+ validated.authorizedBySessionId,
1276
+ );
1277
+ return {
1278
+ matched: true,
1279
+ deletion: this.startSessionDeletion(
1280
+ state,
1281
+ scopeId,
1282
+ scope.scope,
1283
+ validated.root.sessionId,
1284
+ ),
1285
+ };
1286
+ });
1287
+ if (!plan.matched) return publicSnapshot({ matched: false });
1288
+ if (!plan.deletion) throw new Error('Matched session deletion has no cleanup operation.');
1289
+ await plan.deletion;
1290
+ return publicSnapshot({ matched: true });
1291
+ }
1292
+
1128
1293
  public async deleteSession(scopeId: string, sessionId: string): Promise<void> {
1129
1294
  this.assertNoAmbientSlashCommandTeardown({ scopeId, sessionId });
1130
1295
  const { scope, state } = await this.resolveState(scopeId);
1131
1296
  this.assertNoAmbientSlashCommandTeardown({ storageKey: state.storageKey });
1132
1297
  validateIdentifier(sessionId, 'sessionId');
1133
1298
  await state.scopeQueue;
1299
+ return this.startSessionDeletion(state, scopeId, scope.scope, sessionId);
1300
+ }
1301
+
1302
+ private startSessionDeletion(
1303
+ state: IStorageState,
1304
+ scopeId: string,
1305
+ scope: TScope,
1306
+ sessionId: string,
1307
+ ): Promise<void> {
1134
1308
  const deletionKey = state.tombstones.get(sessionId)?.rootSessionId ?? sessionId;
1309
+ return this.registerSessionDeletion(
1310
+ state,
1311
+ deletionKey,
1312
+ () => this.deleteSessionInternal(state, scopeId, scope, sessionId),
1313
+ );
1314
+ }
1315
+
1316
+ private startReservedSessionDeletion(
1317
+ state: IStorageState,
1318
+ scopeId: string,
1319
+ scope: TScope,
1320
+ sessionId: string,
1321
+ reservation: IReservedSessionDeletion,
1322
+ ): Promise<void> {
1323
+ return this.registerSessionDeletion(
1324
+ state,
1325
+ sessionId,
1326
+ () => this.finishReservedSessionDeletion(
1327
+ state,
1328
+ scopeId,
1329
+ scope,
1330
+ sessionId,
1331
+ reservation,
1332
+ ),
1333
+ );
1334
+ }
1335
+
1336
+ private registerSessionDeletion(
1337
+ state: IStorageState,
1338
+ deletionKey: string,
1339
+ operation: () => Promise<void>,
1340
+ ): Promise<void> {
1135
1341
  const existingDeletion = state.sessionDeletions.get(deletionKey);
1136
1342
  if (existingDeletion) return existingDeletion;
1137
1343
  let deletion!: Promise<void>;
1138
- deletion = this.deleteSessionInternal(state, scopeId, scope.scope, sessionId).finally(() => {
1344
+ deletion = operation().finally(() => {
1139
1345
  if (state.sessionDeletions.get(deletionKey) === deletion) {
1140
1346
  state.sessionDeletions.delete(deletionKey);
1141
1347
  }
@@ -1144,6 +1350,60 @@ export class FlexHarness<TScope = unknown> {
1144
1350
  return deletion;
1145
1351
  }
1146
1352
 
1353
+ private assertAuthorizedSessionGenerationCohort(
1354
+ entries: readonly (IFlexSession | IFlexSessionTombstone)[],
1355
+ authorizedBySessionId: ReadonlyMap<
1356
+ string,
1357
+ Readonly<IFlexSessionGenerationCohortEntry>
1358
+ >,
1359
+ ): void {
1360
+ for (const entry of entries) {
1361
+ const authorized = authorizedBySessionId.get(entry.sessionId);
1362
+ if (!authorized || !matchesSessionGeneration(entry, authorized)) {
1363
+ throw new FlexHarnessValidationError(
1364
+ `authorizedCohort does not authorize the exact generation of session "${entry.sessionId}".`,
1365
+ );
1366
+ }
1367
+ }
1368
+ }
1369
+
1370
+ private reserveLiveSessionDeletion(
1371
+ state: IStorageState,
1372
+ sessionId: string,
1373
+ stored: IStoredSessionState,
1374
+ subtree: readonly IStoredSessionState[],
1375
+ descendantRoots: readonly string[],
1376
+ ): IReservedSessionDeletion {
1377
+ if (state.sessions.get(sessionId) !== stored) {
1378
+ throw new FlexHarnessNotFoundError('Session', sessionId);
1379
+ }
1380
+ const rootDepth = stored.session.depth ?? 0;
1381
+ const deletedAt = new Date().toISOString();
1382
+ const deletedSessions = subtree.map((entry) => cloneSerializable(entry.session));
1383
+ for (const entry of subtree) {
1384
+ const entrySessionId = entry.session.sessionId;
1385
+ state.sessions.delete(entrySessionId);
1386
+ state.retainedSessionCleanups.set(entrySessionId, {
1387
+ stored: entry,
1388
+ domainsCompleted: false,
1389
+ });
1390
+ state.tombstones.set(entrySessionId, {
1391
+ sessionId: entrySessionId,
1392
+ ...requireSessionGeneration(entry.session),
1393
+ deletedAt,
1394
+ rootSessionId: sessionId,
1395
+ depth: (entry.session.depth ?? rootDepth) - rootDepth,
1396
+ ...(entry.session.parentSessionId === undefined
1397
+ ? {}
1398
+ : { parentSessionId: entry.session.parentSessionId }),
1399
+ });
1400
+ }
1401
+ return {
1402
+ deletedSessions,
1403
+ descendantRoots: [...descendantRoots],
1404
+ };
1405
+ }
1406
+
1147
1407
  private async deleteSessionInternal(
1148
1408
  state: IStorageState,
1149
1409
  scopeId: string,
@@ -1173,36 +1433,28 @@ export class FlexHarness<TScope = unknown> {
1173
1433
  return;
1174
1434
  }
1175
1435
  const stored = this.requireSession(state, sessionId);
1176
- let deletedSessions: IFlexSession[] = [];
1177
- let descendantRoots: string[] = [];
1178
- await this.mutateScope(state, () => {
1179
- if (state.sessions.get(sessionId) !== stored) {
1180
- throw new FlexHarnessNotFoundError('Session', sessionId);
1181
- }
1436
+ const reservation = await this.mutateScope(state, () => {
1182
1437
  const subtree = this.collectSessionSubtree(state, sessionId);
1183
- descendantRoots = this.descendantTombstoneRoots(state, sessionId);
1184
- const rootDepth = stored.session.depth ?? 0;
1185
- const deletedAt = new Date().toISOString();
1186
- deletedSessions = subtree.map((entry) => cloneSerializable(entry.session));
1187
- for (const entry of subtree) {
1188
- const entrySessionId = entry.session.sessionId;
1189
- state.sessions.delete(entrySessionId);
1190
- state.retainedSessionCleanups.set(entrySessionId, {
1191
- stored: entry,
1192
- domainsCompleted: false,
1193
- });
1194
- state.tombstones.set(entrySessionId, {
1195
- sessionId: entrySessionId,
1196
- ...requireSessionGeneration(entry.session),
1197
- deletedAt,
1198
- rootSessionId: sessionId,
1199
- depth: (entry.session.depth ?? rootDepth) - rootDepth,
1200
- ...(entry.session.parentSessionId === undefined
1201
- ? {}
1202
- : { parentSessionId: entry.session.parentSessionId }),
1203
- });
1204
- }
1438
+ const descendantRoots = this.descendantTombstoneRoots(state, sessionId);
1439
+ return this.reserveLiveSessionDeletion(
1440
+ state,
1441
+ sessionId,
1442
+ stored,
1443
+ subtree,
1444
+ descendantRoots,
1445
+ );
1205
1446
  });
1447
+ await this.finishReservedSessionDeletion(state, scopeId, scope, sessionId, reservation);
1448
+ }
1449
+
1450
+ private async finishReservedSessionDeletion(
1451
+ state: IStorageState,
1452
+ scopeId: string,
1453
+ scope: TScope,
1454
+ sessionId: string,
1455
+ reservation: IReservedSessionDeletion,
1456
+ ): Promise<void> {
1457
+ const { deletedSessions, descendantRoots } = reservation;
1206
1458
  const reason = this.trustInternalError(new FlexHarnessAbortError('The session subtree was deleted.'));
1207
1459
  const slashCommandErrors: unknown[] = [];
1208
1460
  for (const deleted of deletedSessions) {
@@ -1251,7 +1503,7 @@ export class FlexHarness<TScope = unknown> {
1251
1503
  throw this.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-delete');
1252
1504
  }
1253
1505
  for (const deleted of childFirstDeletedSessions) {
1254
- this.emitEvent(scopeId, deleted.sessionId, {
1506
+ this.emitEvent(scopeId, deleted, {
1255
1507
  type: 'session.deleted',
1256
1508
  session: publicSnapshot(deleted),
1257
1509
  });
@@ -1581,7 +1833,7 @@ export class FlexHarness<TScope = unknown> {
1581
1833
  promptOptions,
1582
1834
  undefined,
1583
1835
  undefined,
1584
- false,
1836
+ undefined,
1585
1837
  signal,
1586
1838
  true,
1587
1839
  );
@@ -2416,7 +2668,7 @@ export class FlexHarness<TScope = unknown> {
2416
2668
  .find((segment) => segment?.status === 'completed');
2417
2669
  if (!target) throw new FlexHarnessValidationError('Pending reversion has no target candidate.');
2418
2670
  await this.resumePendingApply(state, stored, scopeId, scope, operationSignal);
2419
- this.emitEvent(scopeId, sessionId, {
2671
+ this.emitEvent(scopeId, stored.session, {
2420
2672
  type: 'session.history.changed',
2421
2673
  direction,
2422
2674
  runId: target.runId,
@@ -2467,7 +2719,7 @@ export class FlexHarness<TScope = unknown> {
2467
2719
  scope,
2468
2720
  operationSignal,
2469
2721
  );
2470
- this.emitEvent(scopeId, sessionId, {
2722
+ this.emitEvent(scopeId, stored.session, {
2471
2723
  type: 'session.history.changed',
2472
2724
  direction,
2473
2725
  runId: unit.target.runId,
@@ -2765,7 +3017,7 @@ export class FlexHarness<TScope = unknown> {
2765
3017
  throw this.projectOperationError(error, 'persistence', scopeId, sessionId, 'archive-horizon');
2766
3018
  }
2767
3019
  if (branchCommitted) {
2768
- this.emitEvent(scopeId, sessionId, {
3020
+ this.emitEvent(scopeId, stored.session, {
2769
3021
  type: 'session.history.changed',
2770
3022
  direction: 'branch',
2771
3023
  });
@@ -2887,7 +3139,7 @@ export class FlexHarness<TScope = unknown> {
2887
3139
  options: IFlexPromptOptions,
2888
3140
  scheduleKey?: string,
2889
3141
  debounceMs?: number,
2890
- subagentAdmission = false,
3142
+ delegationSeed?: IDelegatedRunAdmissionSeed,
2891
3143
  admissionSignal?: AbortSignal,
2892
3144
  slashCommandAdmission = false,
2893
3145
  ): Promise<{
@@ -2947,11 +3199,14 @@ export class FlexHarness<TScope = unknown> {
2947
3199
  if (stored.pendingReversion) {
2948
3200
  throw new FlexHarnessSessionBusyError(sessionId, 'has a pending reversion operation');
2949
3201
  }
2950
- if (stored.session.agent !== undefined && !subagentAdmission) {
3202
+ if (stored.session.agent !== undefined && delegationSeed === undefined) {
2951
3203
  throw new FlexHarnessValidationError(
2952
3204
  'Subagent sessions can only be prompted through the foreground delegate tool.',
2953
3205
  );
2954
3206
  }
3207
+ if (delegationSeed !== undefined) {
3208
+ this.assertDelegatedRunAdmissionOwnership(delegationSeed, state, stored, scopeId);
3209
+ }
2955
3210
  if (stored.outstandingPromptsById.size >= this.promptQueueLimits.maxOutstandingPromptsPerSession) {
2956
3211
  throw new FlexHarnessQueueFullError(
2957
3212
  `Session "${sessionId}" has reached its outstanding prompt limit.`,
@@ -2990,7 +3245,7 @@ export class FlexHarness<TScope = unknown> {
2990
3245
  state,
2991
3246
  stored,
2992
3247
  scopeId,
2993
- scope: resolved.scope.scope,
3248
+ scope: delegationSeed === undefined ? resolved.scope.scope : delegationSeed.scope,
2994
3249
  sessionId,
2995
3250
  queueId: plugins.crypto.randomUUID(),
2996
3251
  queueSequence: ++this.promptQueueSequence,
@@ -2999,6 +3254,7 @@ export class FlexHarness<TScope = unknown> {
2999
3254
  ...(scheduleKey === undefined ? {} : { scheduleKey, debounceMs }),
3000
3255
  prompt: normalizedPrompt,
3001
3256
  options: normalizedOptions,
3257
+ ...(delegationSeed === undefined ? {} : { delegationSeed }),
3002
3258
  byteSize,
3003
3259
  completion,
3004
3260
  resolveCompletion,
@@ -3120,6 +3376,7 @@ export class FlexHarness<TScope = unknown> {
3120
3376
  callbacksClosed: false,
3121
3377
  reasoningPartIds: new Map(),
3122
3378
  toolPartIds: new Map(),
3379
+ trustedToolExecutionErrors: new Map(),
3123
3380
  subagentCallCount: 0,
3124
3381
  subagentSessionIds: new Set(),
3125
3382
  pendingPermissionIds: new Set(),
@@ -3138,6 +3395,258 @@ export class FlexHarness<TScope = unknown> {
3138
3395
  || run.stored.outstandingPromptsById.get(run.queueId) !== queued
3139
3396
  || run.state.activeRuns.get(run.sessionId) !== run
3140
3397
  ) throw this.trustInternalError(new FlexHarnessAbortError('The prompt was retired during admission.'));
3398
+ if (queued.delegationSeed) {
3399
+ this.assertDelegatedRunAdmissionOwnership(
3400
+ queued.delegationSeed,
3401
+ run.state,
3402
+ run.stored,
3403
+ run.scopeId,
3404
+ );
3405
+ if (
3406
+ run.queueId !== queued.queueId
3407
+ || run.runId !== queued.runId
3408
+ || run.sessionId !== queued.delegationSeed.sessionId
3409
+ ) {
3410
+ throw this.trustInternalError(
3411
+ new FlexHarnessAbortError('The delegated prompt lost its exact child run ownership.'),
3412
+ );
3413
+ }
3414
+ }
3415
+ }
3416
+
3417
+ private assertDelegatedRunAdmissionOwnership(
3418
+ seed: IDelegatedRunAdmissionSeed,
3419
+ state: IStorageState,
3420
+ childStored: IStoredSessionState,
3421
+ scopeId: string,
3422
+ ): void {
3423
+ const child = childStored.session;
3424
+ const parentRun = seed.parentRun;
3425
+ const parent = seed.parentStored.session;
3426
+ const parentPartId = parentRun.toolPartIds.get(seed.parentToolCallId);
3427
+ const parentPart = parentRun.callbackParts.find((part): part is IFlexToolMessagePart =>
3428
+ part.type === 'tool'
3429
+ && part.partId === parentPartId
3430
+ && part.toolCallId === seed.parentToolCallId);
3431
+ if (
3432
+ seed.state !== state
3433
+ || seed.storageKey !== state.storageKey
3434
+ || seed.scopeId !== scopeId
3435
+ || seed.childStored !== childStored
3436
+ || childStored.storageKey !== seed.storageKey
3437
+ || state.sessions.get(seed.sessionId) !== childStored
3438
+ || state.tombstones.has(seed.sessionId)
3439
+ || state.initializingSessions.has(seed.sessionId)
3440
+ || child.sessionId !== seed.sessionId
3441
+ || child.sessionGenerationId !== seed.sessionGenerationId
3442
+ || child.sessionGenerationSequence !== seed.sessionGenerationSequence
3443
+ || child.parentSessionId !== seed.parentSessionId
3444
+ || child.parentRunId !== seed.originParentRunId
3445
+ || child.parentToolCallId !== seed.originParentToolCallId
3446
+ || child.agent !== seed.agent
3447
+ || child.depth !== seed.depth
3448
+ || seed.parentStored !== parentRun.stored
3449
+ || seed.parentStored.storageKey !== seed.storageKey
3450
+ || parentRun.state !== state
3451
+ || parentRun.scopeId !== seed.scopeId
3452
+ || parentRun.scope !== seed.scope
3453
+ || parentRun.sessionId !== seed.parentSessionId
3454
+ || parentRun.queueId !== seed.parentQueueId
3455
+ || parentRun.runId !== seed.parentRunId
3456
+ || state.sessions.get(seed.parentSessionId) !== seed.parentStored
3457
+ || state.tombstones.has(seed.parentSessionId)
3458
+ || parent.sessionId !== seed.parentSessionId
3459
+ || parent.sessionGenerationId !== seed.parentSessionGenerationId
3460
+ || parent.sessionGenerationSequence !== seed.parentSessionGenerationSequence
3461
+ || state.activeRuns.get(seed.parentSessionId) !== parentRun
3462
+ || parentRun.callbacksClosed
3463
+ || parentRun.controller.signal.aborted
3464
+ || parentPart?.status !== 'running'
3465
+ ) {
3466
+ throw this.trustInternalError(
3467
+ new FlexHarnessAbortError('The delegated prompt lost its exact parent or child ownership.'),
3468
+ );
3469
+ }
3470
+ }
3471
+
3472
+ private async acquireDelegatedRunAdmission(
3473
+ queued: IQueuedPrompt,
3474
+ run: IActiveRun,
3475
+ ): Promise<void> {
3476
+ const seed = queued.delegationSeed;
3477
+ const provider = this.delegatedRunAdmissionProvider;
3478
+ if (!seed || !provider) return;
3479
+ const context = Object.freeze({
3480
+ scopeId: seed.scopeId,
3481
+ scope: seed.scope as TScope,
3482
+ storageKey: seed.storageKey,
3483
+ sessionId: seed.sessionId,
3484
+ sessionGenerationId: seed.sessionGenerationId,
3485
+ sessionGenerationSequence: seed.sessionGenerationSequence,
3486
+ queueId: queued.queueId,
3487
+ runId: run.runId,
3488
+ parentSessionId: seed.parentSessionId,
3489
+ parentSessionGenerationId: seed.parentSessionGenerationId,
3490
+ parentSessionGenerationSequence: seed.parentSessionGenerationSequence,
3491
+ parentQueueId: seed.parentQueueId,
3492
+ parentRunId: seed.parentRunId,
3493
+ parentToolCallId: seed.parentToolCallId,
3494
+ agent: seed.agent,
3495
+ depth: seed.depth,
3496
+ signal: run.controller.signal,
3497
+ });
3498
+ const owner: IDelegatedRunAdmissionOwner = {
3499
+ storageState: run.state,
3500
+ run,
3501
+ context,
3502
+ status: 'acquiring',
3503
+ acquisitionSettled: false,
3504
+ acquisitionDetached: false,
3505
+ closeAttemptSequence: 0,
3506
+ };
3507
+ run.state.delegatedRunAdmissionOwners.add(owner);
3508
+ run.delegatedRunAdmissionOwner = owner;
3509
+ const acquisition = Promise.resolve()
3510
+ .then(() => provider.acquireDelegatedRunAdmission(context))
3511
+ .then((lease) => this.normalizeDelegatedRunAdmissionLease(lease))
3512
+ .then(
3513
+ (lease) => {
3514
+ owner.lease = lease;
3515
+ owner.acquisitionSettled = true;
3516
+ if (owner.status === 'acquiring') owner.status = 'acquired';
3517
+ if (owner.status === 'close-requested') {
3518
+ void this.startDelegatedRunAdmissionCloseAttempt(owner).catch(() => undefined);
3519
+ }
3520
+ },
3521
+ (error: unknown) => {
3522
+ owner.acquisitionError = this.projectExternalError(
3523
+ run,
3524
+ error,
3525
+ 'delegatedRunAdmissionProvider',
3526
+ );
3527
+ owner.acquisitionSettled = true;
3528
+ owner.status = 'closed';
3529
+ run.state.delegatedRunAdmissionOwners.delete(owner);
3530
+ },
3531
+ );
3532
+ owner.acquisitionCompletion = acquisition;
3533
+
3534
+ let resolveAbort!: () => void;
3535
+ const abort = new Promise<'aborted'>((resolve) => {
3536
+ resolveAbort = () => resolve('aborted');
3537
+ });
3538
+ run.controller.signal.addEventListener('abort', resolveAbort, { once: true });
3539
+ if (run.controller.signal.aborted) resolveAbort();
3540
+ try {
3541
+ const outcome = await Promise.race([
3542
+ acquisition.then(() => 'settled' as const),
3543
+ abort,
3544
+ ]);
3545
+ if (outcome === 'aborted') {
3546
+ owner.acquisitionDetached = true;
3547
+ throw run.controller.signal.reason ?? this.trustInternalError(new FlexHarnessAbortError());
3548
+ }
3549
+ if (run.controller.signal.aborted) {
3550
+ throw run.controller.signal.reason ?? this.trustInternalError(new FlexHarnessAbortError());
3551
+ }
3552
+ if (owner.acquisitionError) throw owner.acquisitionError;
3553
+ if (owner.status !== 'acquired' || !owner.lease) {
3554
+ throw this.trustInternalError(
3555
+ new FlexHarnessAbortError('The delegated run admission was retired during acquisition.'),
3556
+ );
3557
+ }
3558
+ } finally {
3559
+ run.controller.signal.removeEventListener('abort', resolveAbort);
3560
+ }
3561
+ }
3562
+
3563
+ private normalizeDelegatedRunAdmissionLease(
3564
+ value: IFlexDelegatedRunAdmissionLease,
3565
+ ): IFlexDelegatedRunAdmissionLease {
3566
+ if (!value || (typeof value !== 'object' && typeof value !== 'function')) {
3567
+ throw new FlexHarnessValidationError(
3568
+ 'Delegated run admission provider returned an invalid lease.',
3569
+ );
3570
+ }
3571
+ const close = value.close;
3572
+ if (typeof close !== 'function') {
3573
+ throw new FlexHarnessValidationError(
3574
+ 'Delegated run admission provider returned an invalid lease.',
3575
+ );
3576
+ }
3577
+ return Object.freeze({ close: () => close.call(value) });
3578
+ }
3579
+
3580
+ private requestDelegatedRunAdmissionClose(
3581
+ owner: IDelegatedRunAdmissionOwner,
3582
+ ): Promise<void> {
3583
+ const priorAttemptSequence = owner.closeAttemptSequence;
3584
+ const joinedAttempt = owner.closeAttempt;
3585
+ if (owner.status === 'closed') return Promise.resolve();
3586
+ if (owner.status !== 'retrying') owner.status = 'close-requested';
3587
+ return (async () => {
3588
+ await owner.acquisitionCompletion;
3589
+ if (owner.status === 'closed') return;
3590
+ if (joinedAttempt) {
3591
+ await joinedAttempt;
3592
+ return;
3593
+ }
3594
+ if (owner.closeAttempt) {
3595
+ await owner.closeAttempt;
3596
+ return;
3597
+ }
3598
+ if (owner.closeFailure && owner.closeFailure.sequence > priorAttemptSequence) {
3599
+ throw owner.closeFailure.error;
3600
+ }
3601
+ await this.startDelegatedRunAdmissionCloseAttempt(owner);
3602
+ })();
3603
+ }
3604
+
3605
+ private startDelegatedRunAdmissionCloseAttempt(
3606
+ owner: IDelegatedRunAdmissionOwner,
3607
+ ): Promise<void> {
3608
+ if (owner.closeAttempt) return owner.closeAttempt;
3609
+ if (!owner.lease) {
3610
+ throw new Error('Acquired delegated run admission has no lease.');
3611
+ }
3612
+ owner.status = 'retrying';
3613
+ owner.closeFailure = undefined;
3614
+ const sequence = ++owner.closeAttemptSequence;
3615
+ let attempt!: Promise<void>;
3616
+ attempt = Promise.resolve()
3617
+ .then(() => owner.lease!.close())
3618
+ .then(() => {
3619
+ owner.status = 'closed';
3620
+ delete owner.lease;
3621
+ owner.storageState.delegatedRunAdmissionOwners.delete(owner);
3622
+ }, (error: unknown) => {
3623
+ const projected = this.projectExternalError(
3624
+ owner.run,
3625
+ error,
3626
+ 'delegatedRunAdmissionProvider',
3627
+ );
3628
+ owner.closeFailure = { sequence, error: projected };
3629
+ owner.status = 'close-requested';
3630
+ throw projected;
3631
+ })
3632
+ .finally(() => {
3633
+ if (owner.closeAttempt === attempt) owner.closeAttempt = undefined;
3634
+ });
3635
+ owner.closeAttempt = attempt;
3636
+ return attempt;
3637
+ }
3638
+
3639
+ private async closeRunDelegatedRunAdmission(
3640
+ run: IActiveRun,
3641
+ awaitSettlement = true,
3642
+ ): Promise<void> {
3643
+ const owner = run.delegatedRunAdmissionOwner;
3644
+ if (!owner) return;
3645
+ if (!run.delegatedRunAdmissionClose) {
3646
+ run.delegatedRunAdmissionClose = this.requestDelegatedRunAdmissionClose(owner);
3647
+ void run.delegatedRunAdmissionClose.catch(() => undefined);
3648
+ }
3649
+ if (awaitSettlement) await run.delegatedRunAdmissionClose;
3141
3650
  }
3142
3651
 
3143
3652
  private async promoteQueuedPrompt(queued: IQueuedPrompt, run: IActiveRun): Promise<void> {
@@ -3145,13 +3654,19 @@ export class FlexHarness<TScope = unknown> {
3145
3654
  const options = queued.options!;
3146
3655
  let projectionReserved = false;
3147
3656
  try {
3657
+ this.assertPromptPromotion(queued, run);
3658
+ await this.acquireDelegatedRunAdmission(queued, run);
3659
+ this.assertPromptPromotion(queued, run);
3148
3660
  await this.commitRevertedBranch(run.state, run.stored, run.scopeId, run.scope as TScope);
3661
+ this.assertPromptPromotion(queued, run);
3662
+ const generationPrompt = cloneSerializable(prompt.modelMessage.content) as Parameters<
3663
+ plugins.IAgentSession['beginGeneration']
3664
+ >[0];
3665
+ this.assertPromptPromotion(queued, run);
3149
3666
  run.transaction = await this.withRunCompactorContext(
3150
3667
  run,
3151
3668
  () => run.stored.agentSession.beginGeneration(
3152
- cloneSerializable(prompt.modelMessage.content) as Parameters<
3153
- plugins.IAgentSession['beginGeneration']
3154
- >[0],
3669
+ generationPrompt,
3155
3670
  { generationId: run.runId },
3156
3671
  ),
3157
3672
  );
@@ -3213,10 +3728,21 @@ export class FlexHarness<TScope = unknown> {
3213
3728
  let cleanupErrors: unknown[] = [];
3214
3729
  let cleanupFailure: unknown;
3215
3730
  try {
3216
- cleanupErrors = await this.rollbackAdmission(run, projectionReserved, safeError);
3731
+ await this.closeRunDelegatedRunAdmission(
3732
+ run,
3733
+ !run.delegatedRunAdmissionOwner?.acquisitionDetached,
3734
+ );
3735
+ } catch (closeError) {
3736
+ cleanupErrors.push(closeError);
3737
+ }
3738
+ try {
3739
+ cleanupErrors.push(...await this.rollbackAdmission(run, projectionReserved, safeError));
3217
3740
  } catch (rollbackError) {
3218
3741
  cleanupFailure = rollbackError;
3219
3742
  }
3743
+ if (cleanupFailure !== undefined && cleanupErrors.length > 0) {
3744
+ cleanupFailure = combineErrors([cleanupFailure, ...cleanupErrors]);
3745
+ }
3220
3746
  if (run.state.lifecycle === 'fenced' && cleanupFailure === undefined) {
3221
3747
  const combined = combineErrors([safeError, ...cleanupErrors]);
3222
3748
  try {
@@ -3231,14 +3757,14 @@ export class FlexHarness<TScope = unknown> {
3231
3757
  }
3232
3758
 
3233
3759
  const runType = run.scheduleKey ? 'run.scheduled' : 'run.started';
3234
- this.emitEvent(run.scopeId, run.sessionId, {
3760
+ this.emitEvent(run.scopeId, run.stored.session, {
3235
3761
  type: runType,
3236
3762
  runId: run.runId,
3237
3763
  messageId: run.userMessageId,
3238
3764
  session: publicSnapshot(run.stored.session),
3239
3765
  });
3240
3766
  for (const message of [run.reservedUserMessage!, run.reservedAssistantMessage!]) {
3241
- this.emitEvent(run.scopeId, run.sessionId, {
3767
+ this.emitEvent(run.scopeId, run.stored.session, {
3242
3768
  type: 'message.created',
3243
3769
  runId: run.runId,
3244
3770
  messageId: message.messageId,
@@ -3307,7 +3833,7 @@ export class FlexHarness<TScope = unknown> {
3307
3833
  type: 'prompt.queued' | 'prompt.started' | 'prompt.running' | 'prompt.finished',
3308
3834
  entry: IFlexPromptQueueEntry = this.projectPromptQueueEntry(queued),
3309
3835
  ): void {
3310
- this.emitEvent(queued.scopeId, queued.sessionId, {
3836
+ this.emitEvent(queued.scopeId, queued.stored.session, {
3311
3837
  type,
3312
3838
  queueId: queued.queueId,
3313
3839
  ...(entry.runId ? { runId: entry.runId } : {}),
@@ -3328,6 +3854,7 @@ export class FlexHarness<TScope = unknown> {
3328
3854
  stored.outstandingPromptBytes = Math.max(0, stored.outstandingPromptBytes - queued.byteSize);
3329
3855
  delete queued.prompt;
3330
3856
  delete queued.options;
3857
+ delete queued.delegationSeed;
3331
3858
  const terminal: IFlexPromptQueueEntry = {
3332
3859
  ...this.projectPromptQueueEntry(queued),
3333
3860
  status,
@@ -3389,6 +3916,11 @@ export class FlexHarness<TScope = unknown> {
3389
3916
  }
3390
3917
 
3391
3918
  private purgeStoredPromptQueue(stored: IStoredSessionState): void {
3919
+ for (const queued of stored.outstandingPromptsById.values()) {
3920
+ delete queued.prompt;
3921
+ delete queued.options;
3922
+ delete queued.delegationSeed;
3923
+ }
3392
3924
  stored.promptQueue.length = 0;
3393
3925
  stored.outstandingPromptsById.clear();
3394
3926
  stored.terminalPromptQueueEntries.clear();
@@ -3408,6 +3940,7 @@ export class FlexHarness<TScope = unknown> {
3408
3940
  const result = normalizeRunResult(rawResult);
3409
3941
  if (!run.modelResolution) throw new FlexHarnessValidationError('Generation completed without a resolved model.');
3410
3942
  await this.finalizeRunReversion(run, 'completed');
3943
+ await this.closeRunDelegatedRunAdmission(run);
3411
3944
  const terminal = this.buildTerminal(run, 'completed', result);
3412
3945
  try {
3413
3946
  await this.mutateProjection(run.state, run.stored, () => {
@@ -3485,10 +4018,19 @@ export class FlexHarness<TScope = unknown> {
3485
4018
  ? run.ownerCancellation!
3486
4019
  : this.projectExternalError(run, error, generated ? 'persistence' : 'agentSession');
3487
4020
  const errors: unknown[] = [safeError];
4021
+ let reversionFailed = false;
3488
4022
  try {
3489
4023
  await this.finalizeRunReversion(run, cancelled ? 'cancelled' : 'failed');
3490
4024
  } catch (reversionError) {
3491
4025
  errors.push(reversionError);
4026
+ reversionFailed = true;
4027
+ }
4028
+ try {
4029
+ await this.closeRunDelegatedRunAdmission(run);
4030
+ } catch (closeError) {
4031
+ if (!errors.includes(closeError)) errors.push(closeError);
4032
+ }
4033
+ if (reversionFailed) {
3492
4034
  const combined = combineErrors(errors);
3493
4035
  await this.fenceNamespace(run.state, run, combined);
3494
4036
  throw combined;
@@ -3991,6 +4533,7 @@ export class FlexHarness<TScope = unknown> {
3991
4533
  scopeId: run.scopeId,
3992
4534
  scope: run.scope as TScope,
3993
4535
  sessionId: run.sessionId,
4536
+ ...requireSessionGeneration(run.stored.session),
3994
4537
  runId: run.runId,
3995
4538
  ...(options.modelHint ? { modelHint: options.modelHint } : {}),
3996
4539
  ...resolverRelationship,
@@ -4190,7 +4733,7 @@ export class FlexHarness<TScope = unknown> {
4190
4733
  run.stored.session.updatedAt = new Date().toISOString();
4191
4734
  session = publicSnapshot(run.stored.session);
4192
4735
  });
4193
- this.emitEvent(run.scopeId, run.sessionId, {
4736
+ this.emitEvent(run.scopeId, session, {
4194
4737
  type: 'session.updated',
4195
4738
  session,
4196
4739
  });
@@ -4414,15 +4957,27 @@ export class FlexHarness<TScope = unknown> {
4414
4957
  .map((definition) => `- ${definition.name}: ${definition.description}`)
4415
4958
  .join('\n');
4416
4959
  return plugins.tool({
4417
- description: `Run one configured FlexHarness subagent in the foreground and return its final text.\nAvailable subagents:\n${available}`,
4960
+ description: `Run one configured FlexHarness subagent in the foreground and return its final text. Omit taskId to create a new child. Supply taskId only to resume the exact child ID returned by an earlier completed delegate call in a later parent run.\nAvailable subagents:\n${available}`,
4418
4961
  inputSchema: plugins.z.object({
4419
4962
  description: plugins.z.string(),
4420
4963
  prompt: plugins.z.string(),
4421
4964
  subagentType: plugins.z.string(),
4422
- taskId: plugins.z.string().optional(),
4965
+ taskId: plugins.z.string()
4966
+ .describe('Omit for a new child. For a later-run resume, use only the exact taskId returned by an earlier completed delegate call.')
4967
+ .optional(),
4423
4968
  }).strict(),
4424
- execute: (input: IFlexSubagentTaskInput, options?: { toolCallId?: string }) =>
4425
- this.executeSubagentTask(run, input, options?.toolCallId),
4969
+ execute: async (input: IFlexSubagentTaskInput, options?: { toolCallId?: string }) => {
4970
+ try {
4971
+ return await this.executeSubagentTask(run, input, options?.toolCallId);
4972
+ } catch (error) {
4973
+ if (
4974
+ options?.toolCallId
4975
+ && error instanceof FlexHarnessValidationError
4976
+ && this.trustedInternalErrors.has(error)
4977
+ ) run.trustedToolExecutionErrors.set(options.toolCallId, error);
4978
+ throw error;
4979
+ }
4980
+ },
4426
4981
  });
4427
4982
  }
4428
4983
 
@@ -4517,6 +5072,12 @@ export class FlexHarness<TScope = unknown> {
4517
5072
  ...(definition.system === undefined ? {} : { system: definition.system }),
4518
5073
  ...(definition.maxSteps === undefined ? {} : { maxSteps: definition.maxSteps }),
4519
5074
  };
5075
+ const delegationSeed = this.createDelegatedRunAdmissionSeed(
5076
+ run,
5077
+ child,
5078
+ definition,
5079
+ toolCallId,
5080
+ );
4520
5081
  queued = await this.enqueuePromptInternal(
4521
5082
  run.scopeId,
4522
5083
  child.session.sessionId,
@@ -4524,7 +5085,7 @@ export class FlexHarness<TScope = unknown> {
4524
5085
  childOptions,
4525
5086
  undefined,
4526
5087
  undefined,
4527
- true,
5088
+ delegationSeed,
4528
5089
  run.controller.signal,
4529
5090
  );
4530
5091
  admission = await queued.started;
@@ -4578,6 +5139,42 @@ export class FlexHarness<TScope = unknown> {
4578
5139
  }
4579
5140
  }
4580
5141
 
5142
+ private createDelegatedRunAdmissionSeed(
5143
+ run: IActiveRun,
5144
+ child: IStoredSessionState,
5145
+ definition: Readonly<IFlexSubagentDefinition>,
5146
+ toolCallId: string,
5147
+ ): IDelegatedRunAdmissionSeed {
5148
+ const childGeneration = requireSessionGeneration(child.session);
5149
+ const parentGeneration = requireSessionGeneration(run.stored.session);
5150
+ if (!child.session.parentRunId || !child.session.parentToolCallId) {
5151
+ throw new FlexHarnessValidationError('Subagent session origin metadata is missing.');
5152
+ }
5153
+ const seed = Object.freeze({
5154
+ state: run.state,
5155
+ childStored: child,
5156
+ parentRun: run,
5157
+ parentStored: run.stored,
5158
+ scopeId: run.scopeId,
5159
+ scope: run.scope,
5160
+ storageKey: run.state.storageKey,
5161
+ sessionId: child.session.sessionId,
5162
+ ...childGeneration,
5163
+ originParentRunId: child.session.parentRunId,
5164
+ originParentToolCallId: child.session.parentToolCallId,
5165
+ parentSessionId: run.sessionId,
5166
+ parentSessionGenerationId: parentGeneration.sessionGenerationId,
5167
+ parentSessionGenerationSequence: parentGeneration.sessionGenerationSequence,
5168
+ parentQueueId: run.queueId,
5169
+ parentRunId: run.runId,
5170
+ parentToolCallId: toolCallId,
5171
+ agent: definition.name,
5172
+ depth: (run.stored.session.depth ?? 0) + 1,
5173
+ });
5174
+ this.assertDelegatedRunAdmissionOwnership(seed, run.state, child, run.scopeId);
5175
+ return seed;
5176
+ }
5177
+
4581
5178
  private async acquireSubagentSession(
4582
5179
  run: IActiveRun,
4583
5180
  definition: Readonly<IFlexSubagentDefinition>,
@@ -4666,7 +5263,11 @@ export class FlexHarness<TScope = unknown> {
4666
5263
  return;
4667
5264
  }
4668
5265
  if (taskId !== undefined || state.tombstones.has(sessionId)) {
4669
- throw new FlexHarnessNotFoundError('Subagent task', sessionId);
5266
+ throw this.trustInternalError(
5267
+ new FlexHarnessValidationError(
5268
+ 'The supplied taskId does not identify a resumable child. Omit taskId to create a new child, or use only an exact taskId returned by an earlier completed delegate call.',
5269
+ ),
5270
+ );
4670
5271
  }
4671
5272
  const timestamp = new Date().toISOString();
4672
5273
  metadata = {
@@ -4722,7 +5323,7 @@ export class FlexHarness<TScope = unknown> {
4722
5323
  throw aborted;
4723
5324
  }
4724
5325
  state.sessions.set(sessionId, loaded);
4725
- this.emitEvent(run.scopeId, sessionId, {
5326
+ this.emitEvent(run.scopeId, metadata, {
4726
5327
  type: 'session.created',
4727
5328
  session: publicSnapshot(metadata),
4728
5329
  });
@@ -5531,7 +6132,7 @@ export class FlexHarness<TScope = unknown> {
5531
6132
  return;
5532
6133
  }
5533
6134
  await this.mutateProjection(state, stored, () => this.commitRevertedBranchState(stored), true);
5534
- this.emitEvent(scopeId, stored.session.sessionId, {
6135
+ this.emitEvent(scopeId, stored.session, {
5535
6136
  type: 'session.history.changed',
5536
6137
  direction: 'branch',
5537
6138
  });
@@ -6057,7 +6658,7 @@ export class FlexHarness<TScope = unknown> {
6057
6658
  }
6058
6659
  }
6059
6660
  for (const message of [terminal.userMessage, terminal.assistantMessage]) {
6060
- this.emitEvent(run.scopeId, run.sessionId, {
6661
+ this.emitEvent(run.scopeId, run.stored.session, {
6061
6662
  type: 'message.updated',
6062
6663
  runId: run.runId,
6063
6664
  messageId: message.messageId,
@@ -6075,7 +6676,7 @@ export class FlexHarness<TScope = unknown> {
6075
6676
  const status: IFlexRunFinishedEvent['status'] = error
6076
6677
  ? cancelled ? 'cancelled' : 'failed'
6077
6678
  : 'completed';
6078
- this.emitEvent(run.scopeId, run.sessionId, {
6679
+ this.emitEvent(run.scopeId, run.stored.session, {
6079
6680
  type: 'run.finished',
6080
6681
  runId: run.runId,
6081
6682
  messageId: run.assistantMessageId,
@@ -6231,10 +6832,12 @@ export class FlexHarness<TScope = unknown> {
6231
6832
  const part = run.callbackParts.find((entry) => entry.partId === partId && entry.type === 'tool');
6232
6833
  if (!part || part.type !== 'tool' || part.status !== 'running') return;
6233
6834
  try {
6835
+ const trustedExecutionError = run.trustedToolExecutionErrors.get(event.toolCallId);
6836
+ run.trustedToolExecutionErrors.delete(event.toolCallId);
6234
6837
  const output = event.success ? normalizeJsonValue(event.output, this.toolOutputLimits) : undefined;
6235
6838
  const projectedError = event.success
6236
6839
  ? undefined
6237
- : this.projectExternalError(run, event.error, 'toolCallback');
6840
+ : this.projectExternalError(run, trustedExecutionError ?? event.error, 'toolCallback');
6238
6841
  const bytes = event.success ? jsonBytes(output) : Buffer.byteLength(projectedError!.message);
6239
6842
  if (!this.reserveCallbackCapacity(run, 1, bytes, 0)) return;
6240
6843
  if (event.success) {
@@ -6333,6 +6936,7 @@ export class FlexHarness<TScope = unknown> {
6333
6936
  permissionId: plugins.crypto.randomUUID(),
6334
6937
  scopeId: run.scopeId,
6335
6938
  sessionId: run.sessionId,
6939
+ ...requireSessionGeneration(run.stored.session),
6336
6940
  runId: run.runId,
6337
6941
  kind: input.kind,
6338
6942
  description: truncateUtf8(input.description, maxTransferMetadataBytes),
@@ -6381,7 +6985,7 @@ export class FlexHarness<TScope = unknown> {
6381
6985
  return promise;
6382
6986
  }
6383
6987
  if (!pending.settled) {
6384
- this.emitEvent(run.scopeId, run.sessionId, {
6988
+ this.emitEvent(run.scopeId, run.stored.session, {
6385
6989
  type: 'permission.requested',
6386
6990
  runId: run.runId,
6387
6991
  request: publicSnapshot(request),
@@ -6482,7 +7086,7 @@ export class FlexHarness<TScope = unknown> {
6482
7086
  state.pendingPermissions.delete(pending.request.permissionId);
6483
7087
  pending.run.pendingPermissionIds.delete(pending.request.permissionId);
6484
7088
  pending.run.controller.signal.removeEventListener('abort', pending.abortListener);
6485
- this.emitEvent(pending.request.scopeId, pending.request.sessionId, {
7089
+ this.emitEvent(pending.request.scopeId, pending.run.stored.session, {
6486
7090
  type: 'permission.resolved',
6487
7091
  runId: pending.request.runId,
6488
7092
  request: publicSnapshot(pending.request),
@@ -6966,6 +7570,39 @@ export class FlexHarness<TScope = unknown> {
6966
7570
  .filter((error): error is Error => error !== undefined);
6967
7571
  }
6968
7572
 
7573
+ private delegatedRunAdmissionOwnersForGeneration(
7574
+ state: IStorageState,
7575
+ session: Readonly<Pick<
7576
+ IFlexSession,
7577
+ 'sessionId' | 'sessionGenerationId' | 'sessionGenerationSequence'
7578
+ >>,
7579
+ ): IDelegatedRunAdmissionOwner[] {
7580
+ return [...state.delegatedRunAdmissionOwners].filter((owner) =>
7581
+ owner.storageState === state
7582
+ && owner.context.sessionId === session.sessionId
7583
+ && owner.context.sessionGenerationId === session.sessionGenerationId
7584
+ && owner.context.sessionGenerationSequence === session.sessionGenerationSequence);
7585
+ }
7586
+
7587
+ private async settleDelegatedRunAdmissionOwners(
7588
+ owners: readonly IDelegatedRunAdmissionOwner[],
7589
+ ): Promise<Error[]> {
7590
+ const results = await Promise.allSettled(
7591
+ owners.map((owner) => this.requestDelegatedRunAdmissionClose(owner)),
7592
+ );
7593
+ return results.flatMap((result, index) => {
7594
+ if (result.status === 'fulfilled') return [];
7595
+ const error = result.reason instanceof Error
7596
+ ? result.reason
7597
+ : this.projectExternalError(
7598
+ owners[index].run,
7599
+ result.reason,
7600
+ 'delegatedRunAdmissionProvider',
7601
+ );
7602
+ return [error];
7603
+ });
7604
+ }
7605
+
6969
7606
  private projectExternalError(
6970
7607
  run: IActiveRun,
6971
7608
  error: unknown,
@@ -7088,6 +7725,7 @@ export class FlexHarness<TScope = unknown> {
7088
7725
  tombstones: new Map(snapshot.tombstones.map((entry) => [entry.sessionId, entry])),
7089
7726
  tombstoneCleanups: new Map(),
7090
7727
  activeRuns: new Map(),
7728
+ delegatedRunAdmissionOwners: new Set(),
7091
7729
  pendingPermissions: new Map(),
7092
7730
  scopeQueue: Promise.resolve(),
7093
7731
  lifecycle: 'active',
@@ -7717,93 +8355,112 @@ export class FlexHarness<TScope = unknown> {
7717
8355
  };
7718
8356
  }
7719
8357
 
8358
+ private withScopeMutationOwnership<TValue>(
8359
+ state: IStorageState,
8360
+ operation: () => TValue | Promise<TValue>,
8361
+ ): Promise<TValue> {
8362
+ const owned = state.scopeQueue.then(async (): Promise<TValue> => {
8363
+ this.assertStateAcceptingWork(state);
8364
+ return operation();
8365
+ });
8366
+ state.scopeQueue = owned.then(() => undefined, () => undefined);
8367
+ return owned;
8368
+ }
8369
+
7720
8370
  private mutateScope<TValue>(
7721
8371
  state: IStorageState,
7722
8372
  mutation: () => TValue,
7723
8373
  allowDuringDrain = false,
7724
8374
  ): Promise<TValue> {
7725
- const operation = state.scopeQueue.then(async () => {
8375
+ const operation = state.scopeQueue.then(() => {
7726
8376
  if (!allowDuringDrain) this.assertStateAcceptingWork(state);
7727
- const beforeRevision = state.revision;
7728
- const beforeSessions = new Map([...state.sessions].map(([id, stored]) => [id, {
7729
- stored,
7730
- metadata: cloneSerializable(stored.session),
7731
- }]));
7732
- const beforeTombstones = new Map([...state.tombstones].map(([id, value]) => [id, cloneSerializable(value)]));
7733
- const beforeRetainedCleanups = new Map(state.retainedSessionCleanups);
7734
- const beforeSnapshot: IFlexScopeSnapshot = {
7735
- schemaVersion: 1,
7736
- revision: beforeRevision,
7737
- sessions: [...beforeSessions.values()]
7738
- .map((entry) => cloneSerializable(entry.metadata))
7739
- .sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
7740
- tombstones: [...beforeTombstones.values()]
7741
- .map((value) => cloneSerializable(value))
7742
- .sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
7743
- };
7744
- const restore = () => {
7745
- state.revision = beforeRevision;
7746
- state.sessions.clear();
7747
- for (const [id, entry] of beforeSessions) {
7748
- entry.stored.session = entry.metadata;
7749
- state.sessions.set(id, entry.stored);
7750
- }
7751
- state.tombstones.clear();
7752
- for (const [id, value] of beforeTombstones) state.tombstones.set(id, value);
7753
- state.retainedSessionCleanups.clear();
7754
- for (const [id, retained] of beforeRetainedCleanups) {
7755
- state.retainedSessionCleanups.set(id, retained);
7756
- }
7757
- };
7758
- let result: TValue;
7759
- try {
7760
- result = mutation();
7761
- } catch (error) {
8377
+ return this.mutateScopeOwned(state, mutation);
8378
+ });
8379
+ state.scopeQueue = operation.then(() => undefined, () => undefined);
8380
+ return operation;
8381
+ }
8382
+
8383
+ private async mutateScopeOwned<TValue>(
8384
+ state: IStorageState,
8385
+ mutation: () => TValue,
8386
+ ): Promise<TValue> {
8387
+ const beforeRevision = state.revision;
8388
+ const beforeSessions = new Map([...state.sessions].map(([id, stored]) => [id, {
8389
+ stored,
8390
+ metadata: cloneSerializable(stored.session),
8391
+ }]));
8392
+ const beforeTombstones = new Map([...state.tombstones].map(([id, value]) => [id, cloneSerializable(value)]));
8393
+ const beforeRetainedCleanups = new Map(state.retainedSessionCleanups);
8394
+ const beforeSnapshot: IFlexScopeSnapshot = {
8395
+ schemaVersion: 1,
8396
+ revision: beforeRevision,
8397
+ sessions: [...beforeSessions.values()]
8398
+ .map((entry) => cloneSerializable(entry.metadata))
8399
+ .sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
8400
+ tombstones: [...beforeTombstones.values()]
8401
+ .map((value) => cloneSerializable(value))
8402
+ .sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
8403
+ };
8404
+ const restore = () => {
8405
+ state.revision = beforeRevision;
8406
+ state.sessions.clear();
8407
+ for (const [id, entry] of beforeSessions) {
8408
+ entry.stored.session = entry.metadata;
8409
+ state.sessions.set(id, entry.stored);
8410
+ }
8411
+ state.tombstones.clear();
8412
+ for (const [id, value] of beforeTombstones) state.tombstones.set(id, value);
8413
+ state.retainedSessionCleanups.clear();
8414
+ for (const [id, retained] of beforeRetainedCleanups) {
8415
+ state.retainedSessionCleanups.set(id, retained);
8416
+ }
8417
+ };
8418
+ let result: TValue;
8419
+ try {
8420
+ result = mutation();
8421
+ } catch (error) {
8422
+ restore();
8423
+ throw error;
8424
+ }
8425
+ const snapshot = this.createScopeSnapshot(state, beforeRevision + 1);
8426
+ try {
8427
+ await this.stores.scopes.save(state.storageKey, snapshot, beforeRevision);
8428
+ state.revision = snapshot.revision;
8429
+ return result;
8430
+ } catch (error) {
8431
+ if (error instanceof FlexHarnessStoreConflictError) {
7762
8432
  restore();
7763
8433
  throw error;
7764
8434
  }
7765
- const snapshot = this.createScopeSnapshot(state, beforeRevision + 1);
7766
- try {
7767
- await this.stores.scopes.save(state.storageKey, snapshot, beforeRevision);
7768
- state.revision = snapshot.revision;
7769
- return result;
7770
- } catch (error) {
7771
- if (error instanceof FlexHarnessStoreConflictError) {
7772
- restore();
7773
- throw error;
7774
- }
7775
- if (error instanceof FlexHarnessStoreCommitUncertainError) {
7776
- state.revision = snapshot.revision;
7777
- state.lifecycle = 'fenced';
7778
- throw error;
7779
- }
7780
- let current: IFlexScopeSnapshot | undefined;
7781
- let reconciliationError: unknown;
7782
- try {
7783
- current = await this.stores.scopes.load(state.storageKey);
7784
- } catch (loadError) {
7785
- reconciliationError = loadError;
7786
- }
7787
- if (current && JSON.stringify(current) === JSON.stringify(snapshot)) {
7788
- state.revision = snapshot.revision;
7789
- throw error;
7790
- }
7791
- if (
7792
- (current === undefined && beforeRevision === 0)
7793
- || (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot))
7794
- ) {
7795
- restore();
7796
- throw error;
7797
- }
8435
+ if (error instanceof FlexHarnessStoreCommitUncertainError) {
7798
8436
  state.revision = snapshot.revision;
7799
8437
  state.lifecycle = 'fenced';
7800
- throw reconciliationError === undefined
7801
- ? error
7802
- : combineErrors([error, reconciliationError]);
8438
+ throw error;
7803
8439
  }
7804
- });
7805
- state.scopeQueue = operation.then(() => undefined, () => undefined);
7806
- return operation;
8440
+ let current: IFlexScopeSnapshot | undefined;
8441
+ let reconciliationError: unknown;
8442
+ try {
8443
+ current = await this.stores.scopes.load(state.storageKey);
8444
+ } catch (loadError) {
8445
+ reconciliationError = loadError;
8446
+ }
8447
+ if (current && JSON.stringify(current) === JSON.stringify(snapshot)) {
8448
+ state.revision = snapshot.revision;
8449
+ throw error;
8450
+ }
8451
+ if (
8452
+ (current === undefined && beforeRevision === 0)
8453
+ || (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot))
8454
+ ) {
8455
+ restore();
8456
+ throw error;
8457
+ }
8458
+ state.revision = snapshot.revision;
8459
+ state.lifecycle = 'fenced';
8460
+ throw reconciliationError === undefined
8461
+ ? error
8462
+ : combineErrors([error, reconciliationError]);
8463
+ }
7807
8464
  }
7808
8465
 
7809
8466
  private mutateProjection<TValue>(
@@ -8237,6 +8894,14 @@ export class FlexHarness<TScope = unknown> {
8237
8894
  || left.sessionId.localeCompare(right.sessionId));
8238
8895
  }
8239
8896
 
8897
+ private tombstonesForRoots(
8898
+ state: IStorageState,
8899
+ rootSessionIds: readonly string[],
8900
+ ): IFlexSessionTombstone[] {
8901
+ return [...new Set(rootSessionIds)]
8902
+ .flatMap((rootSessionId) => this.tombstoneGroup(state, rootSessionId));
8903
+ }
8904
+
8240
8905
  private descendantTombstoneRoots(
8241
8906
  state: IStorageState,
8242
8907
  ancestorSessionId: string,
@@ -8349,6 +9014,9 @@ export class FlexHarness<TScope = unknown> {
8349
9014
  this.appendUnexpectedErrors(errors, settled[0].reason);
8350
9015
  }
8351
9016
  }
9017
+ errors.push(...await this.settleDelegatedRunAdmissionOwners(
9018
+ this.delegatedRunAdmissionOwnersForGeneration(state, tombstone),
9019
+ ));
8352
9020
  if (retained) {
8353
9021
  if (retained.stored.promptQueueDrain) {
8354
9022
  const settled = await Promise.allSettled([retained.stored.promptQueueDrain]);
@@ -8453,7 +9121,11 @@ export class FlexHarness<TScope = unknown> {
8453
9121
  for (const result of runResults) {
8454
9122
  if (result.status === 'rejected') this.appendUnexpectedErrors(cleanupErrors, result.reason);
8455
9123
  }
8456
- if (dependentRuns.length > 0) {
9124
+ if (
9125
+ dependentRuns.length > 0
9126
+ || (currentRun.delegatedRunAdmissionOwner !== undefined
9127
+ && state.delegatedRunAdmissionOwners.has(currentRun.delegatedRunAdmissionOwner))
9128
+ ) {
8457
9129
  state.fenceAdditionalErrors.push(cause, ...cleanupErrors);
8458
9130
  const stateLoad = this.stateLoads.get(state.storageKey);
8459
9131
  if (!stateLoad) {
@@ -8470,6 +9142,9 @@ export class FlexHarness<TScope = unknown> {
8470
9142
  void deferredDrain.catch(() => undefined);
8471
9143
  return;
8472
9144
  }
9145
+ cleanupErrors.push(...await this.settleDelegatedRunAdmissionOwners([
9146
+ ...state.delegatedRunAdmissionOwners,
9147
+ ]));
8473
9148
  const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
8474
9149
  await Promise.allSettled([...state.sessionInitializations.values()]);
8475
9150
  cleanupErrors.push(...await this.closeOrphanedResources(state.storageKey));
@@ -8557,6 +9232,7 @@ export class FlexHarness<TScope = unknown> {
8557
9232
  state.tombstoneCleanups.set(currentTombstoneRoot, currentTombstoneCleanup);
8558
9233
  }
8559
9234
  state.activeRuns.clear();
9235
+ state.delegatedRunAdmissionOwners.clear();
8560
9236
  state.pendingPermissions.clear();
8561
9237
  state.initializingSessions.clear();
8562
9238
  state.sessionInitializations.clear();
@@ -8994,6 +9670,9 @@ export class FlexHarness<TScope = unknown> {
8994
9670
  for (const result of runResults) {
8995
9671
  if (result.status === 'rejected') this.appendUnexpectedErrors(errors, result.reason);
8996
9672
  }
9673
+ errors.push(...await this.settleDelegatedRunAdmissionOwners([
9674
+ ...state.delegatedRunAdmissionOwners,
9675
+ ]));
8997
9676
  for (const stored of state.sessions.values()) {
8998
9677
  try {
8999
9678
  await this.abortStoredSession(
@@ -9092,6 +9771,7 @@ export class FlexHarness<TScope = unknown> {
9092
9771
  state.sessionDeletions.clear();
9093
9772
  state.tombstoneCleanups.clear();
9094
9773
  state.activeRuns.clear();
9774
+ state.delegatedRunAdmissionOwners.clear();
9095
9775
  state.pendingPermissions.clear();
9096
9776
  state.initializingSessions.clear();
9097
9777
  state.sessionInitializations.clear();
@@ -9417,7 +10097,7 @@ export class FlexHarness<TScope = unknown> {
9417
10097
  part: TFlexMessagePart,
9418
10098
  ): void {
9419
10099
  const { messageIndex, partIndex } = this.partEventCoordinates(run, part.partId);
9420
- this.emitEvent(run.scopeId, run.sessionId, {
10100
+ this.emitEvent(run.scopeId, run.stored.session, {
9421
10101
  type,
9422
10102
  runId: run.runId,
9423
10103
  messageId: run.assistantMessageId,
@@ -9435,7 +10115,7 @@ export class FlexHarness<TScope = unknown> {
9435
10115
  accounting: { baseTextUtf8Bytes: number; textUtf8Bytes: number },
9436
10116
  ): void {
9437
10117
  const { messageIndex, partIndex } = this.partEventCoordinates(run, part.partId);
9438
- this.emitEvent(run.scopeId, run.sessionId, {
10118
+ this.emitEvent(run.scopeId, run.stored.session, {
9439
10119
  type: 'part.delta',
9440
10120
  runId: run.runId,
9441
10121
  messageId: run.assistantMessageId,
@@ -9465,14 +10145,16 @@ export class FlexHarness<TScope = unknown> {
9465
10145
  return { messageIndex, partIndex };
9466
10146
  }
9467
10147
 
9468
- private emitEvent(scopeId: string, sessionId: string, details: TEventDetails): void {
10148
+ private emitEvent(scopeId: string, sourceSession: IFlexSession, details: TEventDetails): void {
10149
+ const generation = requireSessionGeneration(sourceSession);
9469
10150
  const event = deepFreeze(cloneSerializable({
9470
10151
  eventId: plugins.crypto.randomUUID(),
9471
10152
  sequence: ++this.sequence,
9472
10153
  timestamp: new Date().toISOString(),
9473
10154
  scopeId,
9474
- sessionId,
9475
10155
  ...details,
10156
+ sessionId: sourceSession.sessionId,
10157
+ ...generation,
9476
10158
  })) as TFlexHarnessEvent;
9477
10159
  for (const listener of this.listeners) {
9478
10160
  try {