@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.
- package/changelog.md +19 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +21 -1
- package/dist_ts/classes.flexharness.js +568 -141
- package/dist_ts/interfaces.d.ts +44 -4
- package/dist_ts/interfaces.js +2 -1
- package/dist_ts/utils.validation.d.ts +6 -1
- package/dist_ts/utils.validation.js +74 -2
- package/package.json +1 -1
- package/readme.md +52 -12
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +824 -142
- package/ts/interfaces.ts +53 -3
- package/ts/utils.validation.ts +125 -1
|
@@ -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(
|
|
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,
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
8375
|
+
const operation = state.scopeQueue.then(() => {
|
|
7726
8376
|
if (!allowDuringDrain) this.assertStateAcceptingWork(state);
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
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
|
-
|
|
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
|
|
7801
|
-
? error
|
|
7802
|
-
: combineErrors([error, reconciliationError]);
|
|
8438
|
+
throw error;
|
|
7803
8439
|
}
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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,
|
|
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 {
|