@modelprofile.com/flexharness 3.3.0 → 3.5.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 +20 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +30 -0
- package/dist_ts/classes.flexharness.js +998 -165
- package/dist_ts/interfaces.d.ts +37 -4
- package/dist_ts/plugins.d.ts +5 -4
- package/dist_ts/plugins.js +5 -4
- package/dist_ts/utils.json.js +176 -8
- package/package.json +1 -1
- package/readme.hints.md +12 -0
- package/readme.md +50 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +1425 -159
- package/ts/interfaces.ts +45 -4
- package/ts/plugins.ts +7 -1
- package/ts/utils.json.ts +202 -8
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
errorToInfo,
|
|
17
17
|
} from './errors.js';
|
|
18
18
|
import type {
|
|
19
|
+
IFlexAgentContextInvocation,
|
|
19
20
|
IFlexAgentSessionPolicy,
|
|
20
21
|
IFlexAttachmentMessagePart,
|
|
21
22
|
IFlexBackgroundExecution,
|
|
@@ -49,6 +50,7 @@ import type {
|
|
|
49
50
|
IFlexScopeSnapshot,
|
|
50
51
|
IFlexSession,
|
|
51
52
|
IFlexSessionTombstone,
|
|
53
|
+
IFlexSubagentDefinition,
|
|
52
54
|
IFlexTerminalProjection,
|
|
53
55
|
IFlexToolHandle,
|
|
54
56
|
IFlexToolMessagePart,
|
|
@@ -96,6 +98,7 @@ type TRunPhase =
|
|
|
96
98
|
|
|
97
99
|
interface IStoredSessionState {
|
|
98
100
|
storageKey: string;
|
|
101
|
+
compactorContext?: IFlexAgentContextInvocation<unknown>;
|
|
99
102
|
session: IFlexSession;
|
|
100
103
|
messages: IFlexMessage[];
|
|
101
104
|
stagedTerminals: IFlexTerminalProjection[];
|
|
@@ -113,6 +116,7 @@ interface IStoredSessionState {
|
|
|
113
116
|
executionContextCloseCompleted: boolean;
|
|
114
117
|
jobStoreReleased: boolean;
|
|
115
118
|
jobs?: NonNullable<plugins.IToolExecutionContext['jobs']>;
|
|
119
|
+
compactorLifecycleController: AbortController;
|
|
116
120
|
promptQueue: IQueuedPrompt[];
|
|
117
121
|
outstandingPromptsById: Map<string, IQueuedPrompt>;
|
|
118
122
|
terminalPromptQueueEntries: Map<string, IFlexPromptQueueEntry>;
|
|
@@ -139,6 +143,12 @@ interface IOrphanedProviderRelease {
|
|
|
139
143
|
release: () => Promise<void>;
|
|
140
144
|
}
|
|
141
145
|
|
|
146
|
+
interface IOrphanedTombstoneCleanup {
|
|
147
|
+
storageKey: string;
|
|
148
|
+
sessionId: string;
|
|
149
|
+
completion: Promise<void>;
|
|
150
|
+
}
|
|
151
|
+
|
|
142
152
|
interface IOrphanedExecutionContextOwner {
|
|
143
153
|
storageKey: string;
|
|
144
154
|
sessionId: string;
|
|
@@ -146,6 +156,7 @@ interface IOrphanedExecutionContextOwner {
|
|
|
146
156
|
|
|
147
157
|
interface IStorageState {
|
|
148
158
|
storageKey: string;
|
|
159
|
+
compactorLifecycleController: AbortController;
|
|
149
160
|
scopeIdHint: string;
|
|
150
161
|
revision: number;
|
|
151
162
|
sessions: Map<string, IStoredSessionState>;
|
|
@@ -167,6 +178,8 @@ interface IStorageState {
|
|
|
167
178
|
interface IActiveRun {
|
|
168
179
|
state: IStorageState;
|
|
169
180
|
stored: IStoredSessionState;
|
|
181
|
+
originCompactorContext: IFlexAgentContextInvocation<unknown>;
|
|
182
|
+
deferredCompactorContext?: IFlexAgentContextInvocation<unknown>;
|
|
170
183
|
scopeId: string;
|
|
171
184
|
scope: unknown;
|
|
172
185
|
sessionId: string;
|
|
@@ -187,6 +200,8 @@ interface IActiveRun {
|
|
|
187
200
|
callbacksClosed: boolean;
|
|
188
201
|
reasoningPartIds: Map<string, string>;
|
|
189
202
|
toolPartIds: Map<string, string>;
|
|
203
|
+
subagentCallCount: number;
|
|
204
|
+
subagentSessionIds: Set<string>;
|
|
190
205
|
pendingPermissionIds: Set<string>;
|
|
191
206
|
phase: TRunPhase;
|
|
192
207
|
reservedUserMessage?: IFlexMessage;
|
|
@@ -269,6 +284,18 @@ interface IExecutableToolRecord extends Record<string, unknown> {
|
|
|
269
284
|
execute?: (input: unknown, options: unknown) => unknown;
|
|
270
285
|
}
|
|
271
286
|
|
|
287
|
+
interface IFlexSubagentTaskInput {
|
|
288
|
+
description: string;
|
|
289
|
+
prompt: string;
|
|
290
|
+
subagentType: string;
|
|
291
|
+
taskId?: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
interface IFlexSubagentAcquisition {
|
|
295
|
+
stored: IStoredSessionState;
|
|
296
|
+
created: boolean;
|
|
297
|
+
}
|
|
298
|
+
|
|
272
299
|
type TEventDetails = Record<string, unknown> & {
|
|
273
300
|
type: TFlexHarnessEvent['type'];
|
|
274
301
|
};
|
|
@@ -297,6 +324,19 @@ const maxProjectedErrorMessageBytes = 2048;
|
|
|
297
324
|
const maxProjectedErrorCodeBytes = 128;
|
|
298
325
|
const maxScheduleDebounceMs = 24 * 60 * 60 * 1000;
|
|
299
326
|
const maxBackgroundExecutions = 100;
|
|
327
|
+
const maxSubagentDefinitions = 32;
|
|
328
|
+
const maxSubagentNameBytes = 128;
|
|
329
|
+
const maxSubagentDescriptionBytes = 2048;
|
|
330
|
+
const maxSubagentModelHintBytes = 512;
|
|
331
|
+
const maxSubagentSystemBytes = 64 * 1024;
|
|
332
|
+
const maxSubagentTaskDescriptionBytes = 256;
|
|
333
|
+
const maxSubagentPromptBytes = 64 * 1024;
|
|
334
|
+
const maxSubagentTaskIdBytes = 512;
|
|
335
|
+
const maxSubagentResultTextBytes = 64 * 1024;
|
|
336
|
+
const defaultMaxSubagentDepth = 1;
|
|
337
|
+
const maximumMaxSubagentDepth = 8;
|
|
338
|
+
const defaultMaxSubagentCallsPerRun = 32;
|
|
339
|
+
const maximumMaxSubagentCallsPerRun = 128;
|
|
300
340
|
const repairCancellationMessage = 'The process stopped before this run completed.';
|
|
301
341
|
const scopeRetirementMessage = 'The scope is being retired.';
|
|
302
342
|
const externalErrorFallback: IFlexErrorInfo = Object.freeze({
|
|
@@ -396,6 +436,102 @@ function validateIdentifier(value: string, name: string): void {
|
|
|
396
436
|
}
|
|
397
437
|
}
|
|
398
438
|
|
|
439
|
+
function validateUtf8String(
|
|
440
|
+
value: unknown,
|
|
441
|
+
name: string,
|
|
442
|
+
maxBytes: number,
|
|
443
|
+
nonEmpty = false,
|
|
444
|
+
): asserts value is string {
|
|
445
|
+
if (
|
|
446
|
+
typeof value !== 'string'
|
|
447
|
+
|| (nonEmpty && !value.trim())
|
|
448
|
+
|| Buffer.byteLength(value, 'utf8') > maxBytes
|
|
449
|
+
) {
|
|
450
|
+
throw new FlexHarnessValidationError(
|
|
451
|
+
`${name} must be ${nonEmpty ? 'a non-empty ' : 'a '}string of at most ${maxBytes} UTF-8 bytes.`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function normalizeSubagents(
|
|
457
|
+
definitions: IFlexSubagentDefinition[] | undefined,
|
|
458
|
+
): ReadonlyMap<string, Readonly<IFlexSubagentDefinition>> {
|
|
459
|
+
if (definitions === undefined) return new Map();
|
|
460
|
+
if (!Array.isArray(definitions) || definitions.length > maxSubagentDefinitions) {
|
|
461
|
+
throw new FlexHarnessValidationError(
|
|
462
|
+
`subagents must be an array with at most ${maxSubagentDefinitions} definitions.`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const normalized = new Map<string, Readonly<IFlexSubagentDefinition>>();
|
|
466
|
+
for (let index = 0; index < definitions.length; index++) {
|
|
467
|
+
const definition = definitions[index];
|
|
468
|
+
if (
|
|
469
|
+
!definition
|
|
470
|
+
|| typeof definition !== 'object'
|
|
471
|
+
|| Array.isArray(definition)
|
|
472
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(definition))
|
|
473
|
+
) {
|
|
474
|
+
throw new FlexHarnessValidationError(`subagents[${index}] must be a plain object.`);
|
|
475
|
+
}
|
|
476
|
+
const unsupported = Object.keys(definition).find(
|
|
477
|
+
(key) => !['name', 'description', 'modelHint', 'system', 'maxSteps'].includes(key),
|
|
478
|
+
);
|
|
479
|
+
if (unsupported) {
|
|
480
|
+
throw new FlexHarnessValidationError(`subagents[${index}] does not support "${unsupported}".`);
|
|
481
|
+
}
|
|
482
|
+
validateUtf8String(definition.name, `subagents[${index}].name`, maxSubagentNameBytes, true);
|
|
483
|
+
validateUtf8String(
|
|
484
|
+
definition.description,
|
|
485
|
+
`subagents[${index}].description`,
|
|
486
|
+
maxSubagentDescriptionBytes,
|
|
487
|
+
);
|
|
488
|
+
if (definition.modelHint !== undefined) {
|
|
489
|
+
validateUtf8String(
|
|
490
|
+
definition.modelHint,
|
|
491
|
+
`subagents[${index}].modelHint`,
|
|
492
|
+
maxSubagentModelHintBytes,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
if (definition.system !== undefined) {
|
|
496
|
+
validateUtf8String(
|
|
497
|
+
definition.system,
|
|
498
|
+
`subagents[${index}].system`,
|
|
499
|
+
maxSubagentSystemBytes,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
if (
|
|
503
|
+
definition.maxSteps !== undefined
|
|
504
|
+
&& (!Number.isSafeInteger(definition.maxSteps) || definition.maxSteps < 1)
|
|
505
|
+
) {
|
|
506
|
+
throw new FlexHarnessValidationError(`subagents[${index}].maxSteps must be a positive integer.`);
|
|
507
|
+
}
|
|
508
|
+
if (normalized.has(definition.name)) {
|
|
509
|
+
throw new FlexHarnessValidationError(`Duplicate subagent definition "${definition.name}".`);
|
|
510
|
+
}
|
|
511
|
+
normalized.set(definition.name, Object.freeze({
|
|
512
|
+
name: definition.name,
|
|
513
|
+
description: definition.description,
|
|
514
|
+
...(definition.modelHint === undefined ? {} : { modelHint: definition.modelHint }),
|
|
515
|
+
...(definition.system === undefined ? {} : { system: definition.system }),
|
|
516
|
+
...(definition.maxSteps === undefined ? {} : { maxSteps: definition.maxSteps }),
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
519
|
+
return Object.freeze(normalized);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function resolveBoundedPositiveInteger(
|
|
523
|
+
value: number | undefined,
|
|
524
|
+
name: string,
|
|
525
|
+
defaultValue: number,
|
|
526
|
+
maximum: number,
|
|
527
|
+
): number {
|
|
528
|
+
const resolved = value ?? defaultValue;
|
|
529
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > maximum) {
|
|
530
|
+
throw new FlexHarnessValidationError(`${name} must be an integer from 1 through ${maximum}.`);
|
|
531
|
+
}
|
|
532
|
+
return resolved;
|
|
533
|
+
}
|
|
534
|
+
|
|
399
535
|
function requireTransferIdentifier(value: string, field: string): void {
|
|
400
536
|
if (Buffer.byteLength(value, 'utf8') > maxTransferIdentifierBytes) {
|
|
401
537
|
throw new FlexHarnessValidationError(`${field} exceeds the transfer limit.`);
|
|
@@ -694,7 +830,9 @@ function resolvePromptQueueLimits(
|
|
|
694
830
|
return resolved;
|
|
695
831
|
}
|
|
696
832
|
|
|
697
|
-
function normalizeAgentSessionPolicy(
|
|
833
|
+
function normalizeAgentSessionPolicy<TScope>(
|
|
834
|
+
policy: IFlexAgentSessionPolicy<TScope> = {},
|
|
835
|
+
): IFlexAgentSessionPolicy<TScope> {
|
|
698
836
|
return {
|
|
699
837
|
...(policy.contextBuilder === undefined ? {} : { contextBuilder: policy.contextBuilder }),
|
|
700
838
|
...(policy.contextCompactor === undefined ? {} : { contextCompactor: policy.contextCompactor }),
|
|
@@ -723,15 +861,19 @@ export class FlexHarness<TScope = unknown> {
|
|
|
723
861
|
private readonly toolProvider: IFlexHarnessOptions<TScope>['toolProvider'];
|
|
724
862
|
private readonly executionContextProvider: IFlexHarnessOptions<TScope>['executionContextProvider'];
|
|
725
863
|
private readonly stores: IFlexHarnessStores;
|
|
726
|
-
private readonly agentSessionPolicy: IFlexAgentSessionPolicy
|
|
864
|
+
private readonly agentSessionPolicy: IFlexAgentSessionPolicy<TScope>;
|
|
727
865
|
private readonly toolOutputLimits: Required<NonNullable<IFlexHarnessOptions<TScope>['toolOutputLimits']>>;
|
|
728
866
|
private readonly callbackLimits: Required<IFlexCallbackLimits>;
|
|
729
867
|
private readonly promptQueueLimits: Required<IFlexPromptQueueLimits>;
|
|
730
868
|
private readonly externalErrorProjector?: TFlexExternalErrorProjector;
|
|
869
|
+
private readonly subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
|
|
870
|
+
private readonly maxSubagentDepth: number;
|
|
871
|
+
private readonly maxSubagentCallsPerRun: number;
|
|
731
872
|
private readonly stateLoads = new Map<string, Promise<IStorageState>>();
|
|
732
873
|
private readonly scopeAdmissions = new Map<string, IScopeAdmissionState>();
|
|
733
874
|
private readonly scopeRetirements = new Map<string, Promise<void>>();
|
|
734
875
|
private readonly storageDrains = new Map<string, Promise<void>>();
|
|
876
|
+
private readonly storageCompactorLifecycleControllers = new Map<string, AbortController>();
|
|
735
877
|
private orphanedResourceQueue: Promise<void> = Promise.resolve();
|
|
736
878
|
private readonly listeners = new Set<TFlexHarnessEventListener>();
|
|
737
879
|
private readonly trustedInternalErrors = new WeakSet<object>();
|
|
@@ -741,7 +883,15 @@ export class FlexHarness<TScope = unknown> {
|
|
|
741
883
|
IOrphanedExecutionContextOwner
|
|
742
884
|
>();
|
|
743
885
|
private readonly orphanedProviderReleases = new Map<string, IOrphanedProviderRelease>();
|
|
886
|
+
private readonly orphanedTombstoneCleanups = new Map<string, IOrphanedTombstoneCleanup>();
|
|
744
887
|
private readonly pendingPromptAdmissionOwners = new Set<IPendingPromptAdmission>();
|
|
888
|
+
private readonly compactorInvocationContext = new plugins.AsyncLocalStorage<
|
|
889
|
+
IFlexAgentContextInvocation<TScope>
|
|
890
|
+
>();
|
|
891
|
+
private readonly deferredCompactorContexts = new WeakMap<
|
|
892
|
+
IFlexAgentContextInvocation<unknown>,
|
|
893
|
+
IFlexAgentContextInvocation<unknown>
|
|
894
|
+
>();
|
|
745
895
|
private sequence = 0;
|
|
746
896
|
private promptQueueSequence = 0;
|
|
747
897
|
private pendingPromptAdmissions = 0;
|
|
@@ -763,6 +913,19 @@ export class FlexHarness<TScope = unknown> {
|
|
|
763
913
|
this.callbackLimits = resolveCallbackLimits(options.callbackLimits);
|
|
764
914
|
this.promptQueueLimits = resolvePromptQueueLimits(options.promptQueueLimits);
|
|
765
915
|
this.externalErrorProjector = options.externalErrorProjector;
|
|
916
|
+
this.subagents = normalizeSubagents(options.subagents);
|
|
917
|
+
this.maxSubagentDepth = resolveBoundedPositiveInteger(
|
|
918
|
+
options.maxSubagentDepth,
|
|
919
|
+
'maxSubagentDepth',
|
|
920
|
+
defaultMaxSubagentDepth,
|
|
921
|
+
maximumMaxSubagentDepth,
|
|
922
|
+
);
|
|
923
|
+
this.maxSubagentCallsPerRun = resolveBoundedPositiveInteger(
|
|
924
|
+
options.maxSubagentCallsPerRun,
|
|
925
|
+
'maxSubagentCallsPerRun',
|
|
926
|
+
defaultMaxSubagentCallsPerRun,
|
|
927
|
+
maximumMaxSubagentCallsPerRun,
|
|
928
|
+
);
|
|
766
929
|
}
|
|
767
930
|
|
|
768
931
|
public async listSessions(scopeId: string): Promise<IFlexSession[]> {
|
|
@@ -828,6 +991,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
828
991
|
updatedAt: timestamp,
|
|
829
992
|
status: 'idle',
|
|
830
993
|
activity: { status: 'idle' },
|
|
994
|
+
depth: 0,
|
|
831
995
|
};
|
|
832
996
|
resolved.state.sessions.set(
|
|
833
997
|
sessionId,
|
|
@@ -841,6 +1005,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
841
1005
|
metadata,
|
|
842
1006
|
scopeId,
|
|
843
1007
|
resolved.scope.scope,
|
|
1008
|
+
placeholder.compactorLifecycleController,
|
|
844
1009
|
);
|
|
845
1010
|
const loaded = await initialization;
|
|
846
1011
|
if (resolved.state.sessions.get(sessionId) !== placeholder) {
|
|
@@ -867,6 +1032,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
867
1032
|
const tombstone: IFlexSessionTombstone = {
|
|
868
1033
|
sessionId,
|
|
869
1034
|
deletedAt: new Date().toISOString(),
|
|
1035
|
+
rootSessionId: sessionId,
|
|
1036
|
+
depth: 0,
|
|
870
1037
|
};
|
|
871
1038
|
let tombstoneCommitted = false;
|
|
872
1039
|
let domainCleanupCompleted = false;
|
|
@@ -876,7 +1043,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
876
1043
|
resolved.state.tombstones.set(sessionId, tombstone);
|
|
877
1044
|
}, true);
|
|
878
1045
|
tombstoneCommitted = true;
|
|
879
|
-
|
|
1046
|
+
completeInitialization();
|
|
1047
|
+
await this.finishTombstoneCleanup(
|
|
1048
|
+
resolved.state,
|
|
1049
|
+
sessionId,
|
|
1050
|
+
scopeId,
|
|
1051
|
+
{ scopeId, scope: resolved.scope.scope },
|
|
1052
|
+
);
|
|
880
1053
|
domainCleanupCompleted = true;
|
|
881
1054
|
} catch (cleanupError) {
|
|
882
1055
|
const projectedCleanup = this.projectOperationError(
|
|
@@ -898,6 +1071,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
898
1071
|
this.trustInternalError(new FlexHarnessAbortError(
|
|
899
1072
|
'The session namespace was fenced after uncertain creation cleanup.',
|
|
900
1073
|
)),
|
|
1074
|
+
{ scopeId, scope: resolved.scope.scope },
|
|
901
1075
|
);
|
|
902
1076
|
} catch {
|
|
903
1077
|
// The projected creation and cleanup errors remain safe for the caller.
|
|
@@ -950,56 +1124,115 @@ export class FlexHarness<TScope = unknown> {
|
|
|
950
1124
|
}
|
|
951
1125
|
|
|
952
1126
|
public async deleteSession(scopeId: string, sessionId: string): Promise<void> {
|
|
953
|
-
const { state } = await this.resolveState(scopeId);
|
|
1127
|
+
const { scope, state } = await this.resolveState(scopeId);
|
|
954
1128
|
validateIdentifier(sessionId, 'sessionId');
|
|
955
|
-
|
|
1129
|
+
await state.scopeQueue;
|
|
1130
|
+
const deletionKey = state.tombstones.get(sessionId)?.rootSessionId ?? sessionId;
|
|
1131
|
+
const existingDeletion = state.sessionDeletions.get(deletionKey);
|
|
956
1132
|
if (existingDeletion) return existingDeletion;
|
|
957
1133
|
let deletion!: Promise<void>;
|
|
958
|
-
deletion = this.deleteSessionInternal(state, scopeId, sessionId).finally(() => {
|
|
959
|
-
if (state.sessionDeletions.get(
|
|
960
|
-
state.sessionDeletions.delete(
|
|
1134
|
+
deletion = this.deleteSessionInternal(state, scopeId, scope.scope, sessionId).finally(() => {
|
|
1135
|
+
if (state.sessionDeletions.get(deletionKey) === deletion) {
|
|
1136
|
+
state.sessionDeletions.delete(deletionKey);
|
|
961
1137
|
}
|
|
962
1138
|
});
|
|
963
|
-
state.sessionDeletions.set(
|
|
1139
|
+
state.sessionDeletions.set(deletionKey, deletion);
|
|
964
1140
|
return deletion;
|
|
965
1141
|
}
|
|
966
1142
|
|
|
967
1143
|
private async deleteSessionInternal(
|
|
968
1144
|
state: IStorageState,
|
|
969
1145
|
scopeId: string,
|
|
1146
|
+
scope: TScope,
|
|
970
1147
|
sessionId: string,
|
|
971
1148
|
): Promise<void> {
|
|
972
1149
|
const existingTombstone = state.tombstones.get(sessionId);
|
|
973
1150
|
if (existingTombstone) {
|
|
1151
|
+
const rootSessionId = existingTombstone.rootSessionId ?? existingTombstone.sessionId;
|
|
1152
|
+
const descendantRoots = this.descendantTombstoneRoots(state, sessionId)
|
|
1153
|
+
.filter((candidate) => candidate !== rootSessionId);
|
|
974
1154
|
try {
|
|
975
|
-
await this.
|
|
1155
|
+
const orphanErrors = await this.closeOrphanedResources(state.storageKey);
|
|
1156
|
+
if (orphanErrors.length > 0) throw combineErrors(orphanErrors);
|
|
1157
|
+
for (const descendantRoot of descendantRoots) {
|
|
1158
|
+
await this.finishTombstoneCleanup(
|
|
1159
|
+
state,
|
|
1160
|
+
descendantRoot,
|
|
1161
|
+
scopeId,
|
|
1162
|
+
{ scopeId, scope },
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
await this.finishTombstoneCleanup(state, rootSessionId, scopeId, { scopeId, scope });
|
|
976
1166
|
} catch (error) {
|
|
977
1167
|
throw this.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-delete');
|
|
978
1168
|
}
|
|
979
1169
|
return;
|
|
980
1170
|
}
|
|
981
1171
|
const stored = this.requireSession(state, sessionId);
|
|
982
|
-
|
|
1172
|
+
let deletedSessions: IFlexSession[] = [];
|
|
1173
|
+
let descendantRoots: string[] = [];
|
|
983
1174
|
await this.mutateScope(state, () => {
|
|
984
1175
|
if (state.sessions.get(sessionId) !== stored) {
|
|
985
1176
|
throw new FlexHarnessNotFoundError('Session', sessionId);
|
|
986
1177
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
sessionId
|
|
994
|
-
|
|
995
|
-
|
|
1178
|
+
const subtree = this.collectSessionSubtree(state, sessionId);
|
|
1179
|
+
descendantRoots = this.descendantTombstoneRoots(state, sessionId);
|
|
1180
|
+
const rootDepth = stored.session.depth ?? 0;
|
|
1181
|
+
const deletedAt = new Date().toISOString();
|
|
1182
|
+
deletedSessions = subtree.map((entry) => cloneSerializable(entry.session));
|
|
1183
|
+
for (const entry of subtree) {
|
|
1184
|
+
const entrySessionId = entry.session.sessionId;
|
|
1185
|
+
state.sessions.delete(entrySessionId);
|
|
1186
|
+
state.retainedSessionCleanups.set(entrySessionId, {
|
|
1187
|
+
stored: entry,
|
|
1188
|
+
domainsCompleted: false,
|
|
1189
|
+
});
|
|
1190
|
+
state.tombstones.set(entrySessionId, {
|
|
1191
|
+
sessionId: entrySessionId,
|
|
1192
|
+
deletedAt,
|
|
1193
|
+
rootSessionId: sessionId,
|
|
1194
|
+
depth: (entry.session.depth ?? rootDepth) - rootDepth,
|
|
1195
|
+
...(entry.session.parentSessionId === undefined
|
|
1196
|
+
? {}
|
|
1197
|
+
: { parentSessionId: entry.session.parentSessionId }),
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
996
1200
|
});
|
|
1201
|
+
const reason = this.trustInternalError(new FlexHarnessAbortError('The session subtree was deleted.'));
|
|
1202
|
+
const childFirstDeletedSessions = [...deletedSessions].sort((left, right) =>
|
|
1203
|
+
(right.depth ?? 0) - (left.depth ?? 0)
|
|
1204
|
+
|| left.sessionId.localeCompare(right.sessionId));
|
|
1205
|
+
for (const deleted of childFirstDeletedSessions) {
|
|
1206
|
+
const active = state.activeRuns.get(deleted.sessionId);
|
|
1207
|
+
if (active) {
|
|
1208
|
+
this.cancelRun(
|
|
1209
|
+
active,
|
|
1210
|
+
reason,
|
|
1211
|
+
this.createCompactorContext(scopeId, scope, state.storageKey, deleted.sessionId),
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
997
1215
|
try {
|
|
998
|
-
await this.
|
|
1216
|
+
const orphanErrors = await this.closeOrphanedResources(state.storageKey);
|
|
1217
|
+
if (orphanErrors.length > 0) throw combineErrors(orphanErrors);
|
|
1218
|
+
for (const descendantRoot of descendantRoots) {
|
|
1219
|
+
await this.finishTombstoneCleanup(
|
|
1220
|
+
state,
|
|
1221
|
+
descendantRoot,
|
|
1222
|
+
scopeId,
|
|
1223
|
+
{ scopeId, scope },
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
await this.finishTombstoneCleanup(state, sessionId, scopeId, { scopeId, scope });
|
|
999
1227
|
} catch (error) {
|
|
1000
1228
|
throw this.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-delete');
|
|
1001
1229
|
}
|
|
1002
|
-
|
|
1230
|
+
for (const deleted of childFirstDeletedSessions) {
|
|
1231
|
+
this.emitEvent(scopeId, deleted.sessionId, {
|
|
1232
|
+
type: 'session.deleted',
|
|
1233
|
+
session: publicSnapshot(deleted),
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1003
1236
|
}
|
|
1004
1237
|
|
|
1005
1238
|
public async getMessages(scopeId: string, sessionId: string): Promise<IFlexMessage[]> {
|
|
@@ -1177,7 +1410,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1177
1410
|
): Promise<boolean> {
|
|
1178
1411
|
validateIdentifier(queueId, 'queueId');
|
|
1179
1412
|
requireTransferIdentifier(queueId, 'queueId');
|
|
1180
|
-
const { state } = await this.resolveState(scopeId);
|
|
1413
|
+
const { scope, state } = await this.resolveState(scopeId);
|
|
1181
1414
|
const stored = this.requireSession(state, sessionId);
|
|
1182
1415
|
const queued = stored.outstandingPromptsById.get(queueId);
|
|
1183
1416
|
if (!queued) {
|
|
@@ -1187,6 +1420,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1187
1420
|
return this.cancelQueuedPrompt(
|
|
1188
1421
|
queued,
|
|
1189
1422
|
this.trustInternalError(new FlexHarnessAbortError('The queued prompt was cancelled.')),
|
|
1423
|
+
this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
|
|
1190
1424
|
);
|
|
1191
1425
|
}
|
|
1192
1426
|
|
|
@@ -1196,20 +1430,28 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1196
1430
|
scheduleKey: string,
|
|
1197
1431
|
): Promise<boolean> {
|
|
1198
1432
|
validateIdentifier(scheduleKey, 'scheduleKey');
|
|
1199
|
-
const { state } = await this.resolveState(scopeId);
|
|
1433
|
+
const { scope, state } = await this.resolveState(scopeId);
|
|
1200
1434
|
const stored = this.requireSession(state, sessionId);
|
|
1201
1435
|
const queued = [...stored.outstandingPromptsById.values()]
|
|
1202
1436
|
.find((entry) => entry.scheduleKey === scheduleKey);
|
|
1203
1437
|
if (!queued) return false;
|
|
1204
1438
|
const cancellation = this.trustInternalError(new FlexHarnessAbortError('The scheduled run was cancelled.'));
|
|
1205
|
-
return this.cancelQueuedPrompt(
|
|
1439
|
+
return this.cancelQueuedPrompt(
|
|
1440
|
+
queued,
|
|
1441
|
+
cancellation,
|
|
1442
|
+
this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
|
|
1443
|
+
);
|
|
1206
1444
|
}
|
|
1207
1445
|
|
|
1208
1446
|
public async abort(scopeId: string, sessionId: string): Promise<boolean> {
|
|
1209
|
-
const { state } = await this.resolveState(scopeId);
|
|
1447
|
+
const { scope, state } = await this.resolveState(scopeId);
|
|
1210
1448
|
const run = state.activeRuns.get(sessionId);
|
|
1211
1449
|
if (!run || run.phase === 'finalizing' || run.phase === 'promoting') return false;
|
|
1212
|
-
this.cancelRun(
|
|
1450
|
+
this.cancelRun(
|
|
1451
|
+
run,
|
|
1452
|
+
this.trustInternalError(new FlexHarnessAbortError()),
|
|
1453
|
+
this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
|
|
1454
|
+
);
|
|
1213
1455
|
return true;
|
|
1214
1456
|
}
|
|
1215
1457
|
|
|
@@ -1233,12 +1475,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1233
1475
|
decision: TFlexPermissionDecision,
|
|
1234
1476
|
): Promise<void> {
|
|
1235
1477
|
validateIdentifier(permissionId, 'permissionId');
|
|
1236
|
-
const { state } = await this.resolveState(scopeId);
|
|
1478
|
+
const { scope, state } = await this.resolveState(scopeId);
|
|
1237
1479
|
const pending = state.pendingPermissions.get(permissionId);
|
|
1238
1480
|
if (!pending || pending.request.sessionId !== sessionId) {
|
|
1239
1481
|
throw new FlexHarnessNotFoundError('Permission', permissionId);
|
|
1240
1482
|
}
|
|
1241
|
-
const
|
|
1483
|
+
const context = this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId);
|
|
1484
|
+
const response = pending.responseQueue.then(() => this.withCompactorContext(
|
|
1485
|
+
context,
|
|
1486
|
+
() => this.applyPermissionResponse(state, pending, decision, context),
|
|
1487
|
+
));
|
|
1242
1488
|
pending.responseQueue = response.then(() => undefined, () => undefined);
|
|
1243
1489
|
return response;
|
|
1244
1490
|
}
|
|
@@ -1275,7 +1521,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1275
1521
|
if (!reconciliation || typeof reconciliation !== 'object' || Array.isArray(reconciliation)) {
|
|
1276
1522
|
throw new FlexHarnessValidationError('Tool execution reconciliation must be a plain object.');
|
|
1277
1523
|
}
|
|
1278
|
-
const { state } = await this.resolveState(scopeId);
|
|
1524
|
+
const { scope, state } = await this.resolveState(scopeId);
|
|
1279
1525
|
const stored = this.requireSession(state, sessionId);
|
|
1280
1526
|
this.assertStateAcceptingWork(state);
|
|
1281
1527
|
let canonical: plugins.TAgentToolExecutionReconciliationOptions;
|
|
@@ -1299,7 +1545,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1299
1545
|
throw new FlexHarnessValidationError('Unknown tool execution reconciliation.');
|
|
1300
1546
|
}
|
|
1301
1547
|
try {
|
|
1302
|
-
await
|
|
1548
|
+
await this.withCompactorContext(
|
|
1549
|
+
this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
|
|
1550
|
+
() => stored.agentSession.reconcileToolExecution(intentId, canonical),
|
|
1551
|
+
);
|
|
1303
1552
|
} catch (error) {
|
|
1304
1553
|
throw this.projectOperationError(error, 'agentSession', scopeId, sessionId, 'tool-reconciliation');
|
|
1305
1554
|
}
|
|
@@ -1311,22 +1560,28 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1311
1560
|
event: IJsonObject & { type: string },
|
|
1312
1561
|
): Promise<void> {
|
|
1313
1562
|
this.validateRuntimeEvent(event);
|
|
1314
|
-
const { state } = await this.resolveState(scopeId);
|
|
1563
|
+
const { scope, state } = await this.resolveState(scopeId);
|
|
1315
1564
|
const stored = this.requireSession(state, sessionId);
|
|
1316
1565
|
this.assertStateAcceptingWork(state);
|
|
1317
1566
|
try {
|
|
1318
|
-
await
|
|
1567
|
+
await this.withCompactorContext(
|
|
1568
|
+
this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
|
|
1569
|
+
() => stored.agentSession.pushRuntimeEvent(cloneSerializable(event)),
|
|
1570
|
+
);
|
|
1319
1571
|
} catch (error) {
|
|
1320
1572
|
throw this.projectOperationError(error, 'agentSession', scopeId, sessionId, 'runtime-event');
|
|
1321
1573
|
}
|
|
1322
1574
|
}
|
|
1323
1575
|
|
|
1324
1576
|
public async compactSession(scopeId: string, sessionId: string): Promise<void> {
|
|
1325
|
-
const { state } = await this.resolveState(scopeId);
|
|
1577
|
+
const { scope, state } = await this.resolveState(scopeId);
|
|
1326
1578
|
const stored = this.requireSession(state, sessionId);
|
|
1327
1579
|
this.assertStateAcceptingWork(state);
|
|
1328
1580
|
try {
|
|
1329
|
-
await
|
|
1581
|
+
await this.withCompactorContext(
|
|
1582
|
+
this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
|
|
1583
|
+
() => stored.agentSession.compact(),
|
|
1584
|
+
);
|
|
1330
1585
|
} catch (error) {
|
|
1331
1586
|
throw this.projectOperationError(error, 'agentSession', scopeId, sessionId, 'compaction');
|
|
1332
1587
|
}
|
|
@@ -1403,11 +1658,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1403
1658
|
): Promise<void> {
|
|
1404
1659
|
validateIdentifier(executionId, 'executionId');
|
|
1405
1660
|
requireTransferIdentifier(executionId, 'executionId');
|
|
1406
|
-
const { state } = await this.resolveState(scopeId);
|
|
1661
|
+
const { scope, state } = await this.resolveState(scopeId);
|
|
1407
1662
|
const stored = this.requireSession(state, sessionId);
|
|
1408
1663
|
this.assertStateAcceptingWork(state);
|
|
1409
1664
|
try {
|
|
1410
|
-
await
|
|
1665
|
+
await this.withCompactorContext(
|
|
1666
|
+
this.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
|
|
1667
|
+
() => stored.agentSession.abortBackgroundExecution(executionId),
|
|
1668
|
+
);
|
|
1411
1669
|
} catch (error) {
|
|
1412
1670
|
throw this.projectOperationError(error, 'agentSession', scopeId, sessionId, 'background-abort');
|
|
1413
1671
|
}
|
|
@@ -1461,6 +1719,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1461
1719
|
options: IFlexPromptOptions,
|
|
1462
1720
|
scheduleKey?: string,
|
|
1463
1721
|
debounceMs?: number,
|
|
1722
|
+
subagentAdmission = false,
|
|
1723
|
+
admissionSignal?: AbortSignal,
|
|
1464
1724
|
): Promise<{ admission: IFlexPromptQueueAdmission; started: Promise<IFlexPromptAdmission> }> {
|
|
1465
1725
|
this.assertOpen();
|
|
1466
1726
|
const normalizedPrompt = normalizeFlexPrompt(prompt);
|
|
@@ -1481,20 +1741,35 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1481
1741
|
resolveSettled: () => resolveSettled(),
|
|
1482
1742
|
};
|
|
1483
1743
|
this.pendingPromptAdmissionOwners.add(pendingOwner);
|
|
1744
|
+
const admissionSignals = [
|
|
1745
|
+
pendingOwner.controller.signal,
|
|
1746
|
+
...(admissionSignal === undefined ? [] : [admissionSignal]),
|
|
1747
|
+
];
|
|
1484
1748
|
let abortAdmission!: () => void;
|
|
1485
1749
|
const abortPromise = new Promise<never>((_resolve, reject) => {
|
|
1486
|
-
abortAdmission = () =>
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1750
|
+
abortAdmission = () => {
|
|
1751
|
+
const aborted = admissionSignals.find((signal) => signal.aborted);
|
|
1752
|
+
reject(aborted?.reason ?? new FlexHarnessAbortError());
|
|
1753
|
+
};
|
|
1754
|
+
for (const signal of admissionSignals) {
|
|
1755
|
+
signal.addEventListener('abort', abortAdmission, { once: true });
|
|
1756
|
+
}
|
|
1757
|
+
if (admissionSignals.some((signal) => signal.aborted)) abortAdmission();
|
|
1491
1758
|
});
|
|
1492
1759
|
try {
|
|
1493
1760
|
const resolved = await Promise.race([this.resolveState(scopeId), abortPromise]);
|
|
1494
1761
|
const state = resolved.state;
|
|
1495
1762
|
await Promise.race([state.scopeQueue, abortPromise]);
|
|
1763
|
+
if (admissionSignal?.aborted) {
|
|
1764
|
+
throw admissionSignal.reason ?? new FlexHarnessAbortError();
|
|
1765
|
+
}
|
|
1496
1766
|
this.assertStateAcceptingWork(state);
|
|
1497
1767
|
const stored = this.requireSession(state, sessionId);
|
|
1768
|
+
if (stored.session.agent !== undefined && !subagentAdmission) {
|
|
1769
|
+
throw new FlexHarnessValidationError(
|
|
1770
|
+
'Subagent sessions can only be prompted through the foreground task tool.',
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1498
1773
|
if (stored.outstandingPromptsById.size >= this.promptQueueLimits.maxOutstandingPromptsPerSession) {
|
|
1499
1774
|
throw new FlexHarnessQueueFullError(
|
|
1500
1775
|
`Session "${sessionId}" has reached its outstanding prompt limit.`,
|
|
@@ -1560,7 +1835,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1560
1835
|
started,
|
|
1561
1836
|
};
|
|
1562
1837
|
} finally {
|
|
1563
|
-
|
|
1838
|
+
for (const signal of admissionSignals) {
|
|
1839
|
+
signal.removeEventListener('abort', abortAdmission);
|
|
1840
|
+
}
|
|
1564
1841
|
this.pendingPromptAdmissionOwners.delete(pendingOwner);
|
|
1565
1842
|
releasePendingAdmission();
|
|
1566
1843
|
pendingOwner.resolveSettled();
|
|
@@ -1636,6 +1913,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1636
1913
|
return {
|
|
1637
1914
|
state: queued.state,
|
|
1638
1915
|
stored: queued.stored,
|
|
1916
|
+
originCompactorContext: this.createCompactorContext(
|
|
1917
|
+
queued.scopeId,
|
|
1918
|
+
queued.scope as TScope,
|
|
1919
|
+
queued.state.storageKey,
|
|
1920
|
+
queued.sessionId,
|
|
1921
|
+
),
|
|
1639
1922
|
scopeId: queued.scopeId,
|
|
1640
1923
|
scope: queued.scope,
|
|
1641
1924
|
sessionId: queued.sessionId,
|
|
@@ -1653,6 +1936,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1653
1936
|
callbacksClosed: false,
|
|
1654
1937
|
reasoningPartIds: new Map(),
|
|
1655
1938
|
toolPartIds: new Map(),
|
|
1939
|
+
subagentCallCount: 0,
|
|
1940
|
+
subagentSessionIds: new Set(),
|
|
1656
1941
|
pendingPermissionIds: new Set(),
|
|
1657
1942
|
phase: 'admitting',
|
|
1658
1943
|
completion: queued.completion,
|
|
@@ -1676,11 +1961,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1676
1961
|
const options = queued.options!;
|
|
1677
1962
|
let projectionReserved = false;
|
|
1678
1963
|
try {
|
|
1679
|
-
run.transaction = await
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1964
|
+
run.transaction = await this.withRunCompactorContext(
|
|
1965
|
+
run,
|
|
1966
|
+
() => run.stored.agentSession.beginGeneration(
|
|
1967
|
+
cloneSerializable(prompt.modelMessage.content) as Parameters<
|
|
1968
|
+
plugins.IAgentSession['beginGeneration']
|
|
1969
|
+
>[0],
|
|
1970
|
+
{ generationId: run.runId },
|
|
1971
|
+
),
|
|
1684
1972
|
);
|
|
1685
1973
|
this.assertPromptPromotion(queued, run);
|
|
1686
1974
|
const reservation = this.createReservation(run, prompt);
|
|
@@ -1759,13 +2047,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1759
2047
|
prepare: (context: { generationId: string; abortSignal: AbortSignal }) =>
|
|
1760
2048
|
this.prepareGeneration(run, options, context.abortSignal),
|
|
1761
2049
|
};
|
|
1762
|
-
const generation = run.scheduleKey
|
|
2050
|
+
const generation = this.withRunCompactorContext(run, () => run.scheduleKey
|
|
1763
2051
|
? run.stored.agentSession.scheduleGenerate({
|
|
1764
2052
|
key: run.scheduleKey,
|
|
1765
2053
|
debounceMs: run.debounceMs,
|
|
1766
2054
|
...generateOptions,
|
|
1767
2055
|
})
|
|
1768
|
-
: run.stored.agentSession.generate(generateOptions);
|
|
2056
|
+
: run.stored.agentSession.generate(generateOptions));
|
|
1769
2057
|
const execution = this.executeRun(run, generation).then(
|
|
1770
2058
|
(result) => this.finishQueuedPrompt(queued, 'completed', undefined, result),
|
|
1771
2059
|
(error) => this.finishQueuedPrompt(
|
|
@@ -1853,7 +2141,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1853
2141
|
}
|
|
1854
2142
|
}
|
|
1855
2143
|
|
|
1856
|
-
private cancelQueuedPrompt(
|
|
2144
|
+
private cancelQueuedPrompt(
|
|
2145
|
+
queued: IQueuedPrompt,
|
|
2146
|
+
reason: FlexHarnessAbortError,
|
|
2147
|
+
context?: IFlexAgentContextInvocation<TScope>,
|
|
2148
|
+
): boolean {
|
|
1857
2149
|
if (queued.stored.outstandingPromptsById.get(queued.queueId) !== queued) return false;
|
|
1858
2150
|
if (queued.status === 'queued') {
|
|
1859
2151
|
this.finishQueuedPrompt(queued, 'cancelled', reason);
|
|
@@ -1863,7 +2155,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1863
2155
|
const run = queued.state.activeRuns.get(queued.sessionId);
|
|
1864
2156
|
if (!run || run.queueId !== queued.queueId) return false;
|
|
1865
2157
|
if (run.phase === 'finalizing' || run.phase === 'promoting') return false;
|
|
1866
|
-
this.cancelRun(run, reason);
|
|
2158
|
+
this.cancelRun(run, reason, context);
|
|
1867
2159
|
return true;
|
|
1868
2160
|
}
|
|
1869
2161
|
|
|
@@ -1871,10 +2163,15 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1871
2163
|
stored: IStoredSessionState,
|
|
1872
2164
|
reason: FlexHarnessAbortError,
|
|
1873
2165
|
excludedQueueId?: string,
|
|
2166
|
+
context?: IFlexAgentContextInvocation<unknown>,
|
|
1874
2167
|
): void {
|
|
1875
2168
|
for (const queued of [...stored.outstandingPromptsById.values()]) {
|
|
1876
2169
|
if (queued.queueId === excludedQueueId) continue;
|
|
1877
|
-
this.cancelQueuedPrompt(
|
|
2170
|
+
this.cancelQueuedPrompt(
|
|
2171
|
+
queued,
|
|
2172
|
+
reason,
|
|
2173
|
+
context as IFlexAgentContextInvocation<TScope> | undefined,
|
|
2174
|
+
);
|
|
1878
2175
|
}
|
|
1879
2176
|
}
|
|
1880
2177
|
|
|
@@ -1908,7 +2205,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1908
2205
|
if (run.controller.signal.aborted) throw run.controller.signal.reason;
|
|
1909
2206
|
run.phase = 'finalizing';
|
|
1910
2207
|
try {
|
|
1911
|
-
await
|
|
2208
|
+
await this.withRunCompactorContext(
|
|
2209
|
+
run,
|
|
2210
|
+
() => run.stored.agentSession.finalizeGeneration(run.transaction!, 'accepted'),
|
|
2211
|
+
);
|
|
1912
2212
|
} catch (error) {
|
|
1913
2213
|
const projected = this.projectExternalError(run, error, 'agentSession');
|
|
1914
2214
|
await this.fenceNamespace(run.state, run, projected);
|
|
@@ -1978,7 +2278,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1978
2278
|
run.phase = 'finalizing';
|
|
1979
2279
|
const desiredOutcome: TCanonicalOutcome = cancelled || !generated ? 'interrupted' : 'rejected';
|
|
1980
2280
|
try {
|
|
1981
|
-
await
|
|
2281
|
+
await this.withRunCompactorContext(
|
|
2282
|
+
run,
|
|
2283
|
+
() => run.stored.agentSession.finalizeGeneration(run.transaction!, desiredOutcome),
|
|
2284
|
+
);
|
|
1982
2285
|
} catch (finalizationError) {
|
|
1983
2286
|
errors.push(this.projectExternalError(run, finalizationError, 'agentSession'));
|
|
1984
2287
|
}
|
|
@@ -2030,28 +2333,36 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2030
2333
|
queued.status = 'running';
|
|
2031
2334
|
this.emitPromptQueueEvent(queued, 'prompt.running');
|
|
2032
2335
|
}
|
|
2336
|
+
const resolverRelationship = {
|
|
2337
|
+
...(run.stored.session.parentSessionId === undefined
|
|
2338
|
+
? {}
|
|
2339
|
+
: { parentSessionId: run.stored.session.parentSessionId }),
|
|
2340
|
+
...(run.stored.session.agent === undefined ? {} : { agent: run.stored.session.agent }),
|
|
2341
|
+
};
|
|
2033
2342
|
const modelOutcome = Promise.resolve()
|
|
2034
|
-
.then(() => this.modelResolver.resolveModel({
|
|
2343
|
+
.then(() => this.modelResolver.resolveModel(Object.freeze({
|
|
2035
2344
|
scopeId: run.scopeId,
|
|
2036
2345
|
scope: run.scope as TScope,
|
|
2037
2346
|
sessionId: run.sessionId,
|
|
2038
2347
|
runId: run.runId,
|
|
2039
2348
|
...(options.modelHint ? { modelHint: options.modelHint } : {}),
|
|
2349
|
+
...resolverRelationship,
|
|
2040
2350
|
signal,
|
|
2041
|
-
}))
|
|
2351
|
+
})))
|
|
2042
2352
|
.then(
|
|
2043
2353
|
(value): IResolverOutcome<IFlexResolvedModel> => ({ source: 'model', success: true, value }),
|
|
2044
2354
|
(error): IResolverOutcome<IFlexResolvedModel> => ({ source: 'model', success: false, error }),
|
|
2045
2355
|
);
|
|
2046
2356
|
const toolOutcome = Promise.resolve()
|
|
2047
|
-
.then(() => this.toolProvider?.provideTools({
|
|
2357
|
+
.then(() => this.toolProvider?.provideTools(Object.freeze({
|
|
2048
2358
|
scopeId: run.scopeId,
|
|
2049
2359
|
scope: run.scope as TScope,
|
|
2050
2360
|
sessionId: run.sessionId,
|
|
2051
2361
|
runId: run.runId,
|
|
2362
|
+
...resolverRelationship,
|
|
2052
2363
|
signal,
|
|
2053
2364
|
requestPermission: (request) => this.requestPermission(run.state, run, request),
|
|
2054
|
-
}))
|
|
2365
|
+
})))
|
|
2055
2366
|
.then(
|
|
2056
2367
|
(value): IResolverOutcome<IFlexToolHandle | undefined> => ({ source: 'tools', success: true, value }),
|
|
2057
2368
|
(error): IResolverOutcome<IFlexToolHandle | undefined> => ({ source: 'tools', success: false, error }),
|
|
@@ -2118,9 +2429,21 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2118
2429
|
run.modelResolution = model!;
|
|
2119
2430
|
let tools: TFlexAgentToolSet | undefined;
|
|
2120
2431
|
try {
|
|
2121
|
-
|
|
2432
|
+
const providedTools = toolHandle?.tools;
|
|
2433
|
+
if (
|
|
2434
|
+
this.subagents.size > 0
|
|
2435
|
+
&& providedTools
|
|
2436
|
+
&& Object.prototype.hasOwnProperty.call(providedTools, 'task')
|
|
2437
|
+
) {
|
|
2438
|
+
throw new FlexHarnessValidationError('The application tool provider cannot define reserved tool "task".');
|
|
2439
|
+
}
|
|
2440
|
+
const combinedTools: Record<string, unknown> = { ...(providedTools ?? {}) };
|
|
2441
|
+
if (this.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
|
|
2442
|
+
combinedTools.task = this.createSubagentTool(run);
|
|
2443
|
+
}
|
|
2444
|
+
tools = Object.keys(combinedTools).length > 0
|
|
2122
2445
|
? wrapToolSet(
|
|
2123
|
-
|
|
2446
|
+
combinedTools as TFlexAgentToolSet,
|
|
2124
2447
|
this.toolOutputLimits,
|
|
2125
2448
|
(toolError) => this.projectExternalError(run, toolError, 'toolExecution'),
|
|
2126
2449
|
)
|
|
@@ -2168,6 +2491,426 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2168
2491
|
}
|
|
2169
2492
|
}
|
|
2170
2493
|
|
|
2494
|
+
private createSubagentTool(run: IActiveRun): unknown {
|
|
2495
|
+
const available = [...this.subagents.values()]
|
|
2496
|
+
.map((definition) => `- ${definition.name}: ${definition.description}`)
|
|
2497
|
+
.join('\n');
|
|
2498
|
+
return plugins.tool({
|
|
2499
|
+
description: `Run one configured FlexHarness subagent in the foreground and return its final text.\nAvailable subagents:\n${available}`,
|
|
2500
|
+
inputSchema: plugins.z.object({
|
|
2501
|
+
description: plugins.z.string(),
|
|
2502
|
+
prompt: plugins.z.string(),
|
|
2503
|
+
subagentType: plugins.z.string(),
|
|
2504
|
+
taskId: plugins.z.string().optional(),
|
|
2505
|
+
}).strict(),
|
|
2506
|
+
execute: (input: IFlexSubagentTaskInput, options?: { toolCallId?: string }) =>
|
|
2507
|
+
this.executeSubagentTask(run, input, options?.toolCallId),
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
private async executeSubagentTask(
|
|
2512
|
+
run: IActiveRun,
|
|
2513
|
+
input: IFlexSubagentTaskInput,
|
|
2514
|
+
toolCallId: string | undefined,
|
|
2515
|
+
): Promise<{ taskId: string; status: 'completed'; text: string; model: IFlexModelIdentity }> {
|
|
2516
|
+
run.subagentCallCount++;
|
|
2517
|
+
if (run.subagentCallCount > this.maxSubagentCallsPerRun) {
|
|
2518
|
+
throw new FlexHarnessValidationError(
|
|
2519
|
+
`Run "${run.runId}" exceeds maxSubagentCallsPerRun (${this.maxSubagentCallsPerRun}).`,
|
|
2520
|
+
);
|
|
2521
|
+
}
|
|
2522
|
+
validateUtf8String(
|
|
2523
|
+
input.description,
|
|
2524
|
+
'task description',
|
|
2525
|
+
maxSubagentTaskDescriptionBytes,
|
|
2526
|
+
true,
|
|
2527
|
+
);
|
|
2528
|
+
validateUtf8String(input.prompt, 'task prompt', maxSubagentPromptBytes, true);
|
|
2529
|
+
validateUtf8String(input.subagentType, 'subagentType', maxSubagentNameBytes, true);
|
|
2530
|
+
if (input.taskId !== undefined) {
|
|
2531
|
+
validateUtf8String(input.taskId, 'taskId', maxSubagentTaskIdBytes, true);
|
|
2532
|
+
}
|
|
2533
|
+
validateUtf8String(toolCallId, 'task toolCallId', maxTransferIdentifierBytes, true);
|
|
2534
|
+
const definition = this.subagents.get(input.subagentType);
|
|
2535
|
+
if (!definition || (run.stored.session.depth ?? 0) >= this.maxSubagentDepth) {
|
|
2536
|
+
throw new FlexHarnessValidationError(`Subagent "${input.subagentType}" is not available.`);
|
|
2537
|
+
}
|
|
2538
|
+
const reservedSessionId = input.taskId ?? this.createSubagentSessionId(
|
|
2539
|
+
run.state.storageKey,
|
|
2540
|
+
run.sessionId,
|
|
2541
|
+
run.runId,
|
|
2542
|
+
toolCallId,
|
|
2543
|
+
);
|
|
2544
|
+
if (run.subagentSessionIds.has(reservedSessionId)) {
|
|
2545
|
+
throw new FlexHarnessValidationError(
|
|
2546
|
+
`Subagent task "${reservedSessionId}" has already been acquired by this parent run.`,
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
run.subagentSessionIds.add(reservedSessionId);
|
|
2550
|
+
let child: IStoredSessionState | undefined;
|
|
2551
|
+
let childCreated = false;
|
|
2552
|
+
let queued: Awaited<ReturnType<typeof this.enqueuePromptInternal>> | undefined;
|
|
2553
|
+
let admission: IFlexPromptAdmission | undefined;
|
|
2554
|
+
const abortChild = () => {
|
|
2555
|
+
const reason = run.controller.signal.reason instanceof FlexHarnessAbortError
|
|
2556
|
+
? run.controller.signal.reason
|
|
2557
|
+
: this.trustInternalError(new FlexHarnessAbortError('The parent run was aborted.'));
|
|
2558
|
+
const ownedChild = child ?? run.state.sessions.get(reservedSessionId);
|
|
2559
|
+
if (
|
|
2560
|
+
ownedChild
|
|
2561
|
+
&& ownedChild.session.parentSessionId === run.sessionId
|
|
2562
|
+
&& ownedChild.session.agent === definition.name
|
|
2563
|
+
&& run.state.initializingSessions.has(reservedSessionId)
|
|
2564
|
+
) this.abortCompactorLifecycle(ownedChild, reason);
|
|
2565
|
+
const childQueue = queued === undefined
|
|
2566
|
+
? undefined
|
|
2567
|
+
: ownedChild?.outstandingPromptsById.get(queued.admission.queueId);
|
|
2568
|
+
if (childQueue) this.cancelQueuedPrompt(childQueue, reason);
|
|
2569
|
+
else if (admission) {
|
|
2570
|
+
this.abortExactRun(run.state, reservedSessionId, admission.runId, reason);
|
|
2571
|
+
}
|
|
2572
|
+
};
|
|
2573
|
+
run.controller.signal.addEventListener('abort', abortChild, { once: true });
|
|
2574
|
+
if (run.controller.signal.aborted) abortChild();
|
|
2575
|
+
try {
|
|
2576
|
+
await this.requestPermission(run.state, run, {
|
|
2577
|
+
kind: 'subagent.start',
|
|
2578
|
+
description: `Start foreground subagent "${definition.name}": ${input.description}`,
|
|
2579
|
+
toolCallId,
|
|
2580
|
+
metadata: {
|
|
2581
|
+
agent: definition.name,
|
|
2582
|
+
description: input.description,
|
|
2583
|
+
...(input.taskId === undefined ? {} : { taskId: input.taskId }),
|
|
2584
|
+
},
|
|
2585
|
+
});
|
|
2586
|
+
const acquired = await this.acquireSubagentSession(
|
|
2587
|
+
run,
|
|
2588
|
+
definition,
|
|
2589
|
+
toolCallId,
|
|
2590
|
+
input.taskId,
|
|
2591
|
+
reservedSessionId,
|
|
2592
|
+
);
|
|
2593
|
+
child = acquired.stored;
|
|
2594
|
+
childCreated = acquired.created;
|
|
2595
|
+
if (run.controller.signal.aborted) throw run.controller.signal.reason;
|
|
2596
|
+
this.updateSubagentToolPart(run, toolCallId, child.session.sessionId);
|
|
2597
|
+
const childOptions: IFlexPromptOptions = {
|
|
2598
|
+
...(definition.modelHint === undefined ? {} : { modelHint: definition.modelHint }),
|
|
2599
|
+
...(definition.system === undefined ? {} : { system: definition.system }),
|
|
2600
|
+
...(definition.maxSteps === undefined ? {} : { maxSteps: definition.maxSteps }),
|
|
2601
|
+
};
|
|
2602
|
+
queued = await this.enqueuePromptInternal(
|
|
2603
|
+
run.scopeId,
|
|
2604
|
+
child.session.sessionId,
|
|
2605
|
+
input.prompt,
|
|
2606
|
+
childOptions,
|
|
2607
|
+
undefined,
|
|
2608
|
+
undefined,
|
|
2609
|
+
true,
|
|
2610
|
+
run.controller.signal,
|
|
2611
|
+
);
|
|
2612
|
+
admission = await queued.started;
|
|
2613
|
+
const result = await queued.admission.completion;
|
|
2614
|
+
this.updateSubagentToolPart(run, toolCallId, child.session.sessionId, result.model);
|
|
2615
|
+
return {
|
|
2616
|
+
taskId: child.session.sessionId,
|
|
2617
|
+
status: 'completed',
|
|
2618
|
+
text: truncateUtf8(result.assistantMessage.parts
|
|
2619
|
+
.filter((part) => part.type === 'text')
|
|
2620
|
+
.map((part) => part.text)
|
|
2621
|
+
.join(''), maxSubagentResultTextBytes),
|
|
2622
|
+
model: publicSnapshot(result.model),
|
|
2623
|
+
};
|
|
2624
|
+
} catch (error) {
|
|
2625
|
+
if (
|
|
2626
|
+
childCreated
|
|
2627
|
+
&& child
|
|
2628
|
+
&& queued === undefined
|
|
2629
|
+
&& run.state.sessions.get(child.session.sessionId) === child
|
|
2630
|
+
&& !run.state.tombstones.has(child.session.sessionId)
|
|
2631
|
+
) {
|
|
2632
|
+
try {
|
|
2633
|
+
await this.deleteSessionInternal(
|
|
2634
|
+
run.state,
|
|
2635
|
+
run.scopeId,
|
|
2636
|
+
run.scope as TScope,
|
|
2637
|
+
child.session.sessionId,
|
|
2638
|
+
);
|
|
2639
|
+
} catch (cleanupError) {
|
|
2640
|
+
throw combineErrors([error, cleanupError]);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
const childModel = child?.messages
|
|
2644
|
+
.filter((message) => message.runId === admission?.runId && message.role === 'assistant')
|
|
2645
|
+
.at(-1)?.model
|
|
2646
|
+
?? child?.stagedTerminals.find((terminal) => terminal.runId === admission?.runId)?.model;
|
|
2647
|
+
const parentPart = run.callbackParts.find((part) =>
|
|
2648
|
+
part.type === 'tool' && part.toolCallId === toolCallId);
|
|
2649
|
+
if (
|
|
2650
|
+
child
|
|
2651
|
+
&& childModel !== undefined
|
|
2652
|
+
&& parentPart?.type === 'tool'
|
|
2653
|
+
&& parentPart.model === undefined
|
|
2654
|
+
) {
|
|
2655
|
+
this.updateSubagentToolPart(run, toolCallId, child.session.sessionId, childModel);
|
|
2656
|
+
}
|
|
2657
|
+
throw error;
|
|
2658
|
+
} finally {
|
|
2659
|
+
run.controller.signal.removeEventListener('abort', abortChild);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
private async acquireSubagentSession(
|
|
2664
|
+
run: IActiveRun,
|
|
2665
|
+
definition: Readonly<IFlexSubagentDefinition>,
|
|
2666
|
+
toolCallId: string,
|
|
2667
|
+
taskId?: string,
|
|
2668
|
+
reservedSessionId?: string,
|
|
2669
|
+
): Promise<IFlexSubagentAcquisition> {
|
|
2670
|
+
const state = run.state;
|
|
2671
|
+
const sessionId = reservedSessionId ?? taskId ?? this.createSubagentSessionId(
|
|
2672
|
+
state.storageKey,
|
|
2673
|
+
run.sessionId,
|
|
2674
|
+
run.runId,
|
|
2675
|
+
toolCallId,
|
|
2676
|
+
);
|
|
2677
|
+
let metadata: IFlexSession | undefined;
|
|
2678
|
+
let placeholder: IStoredSessionState | undefined;
|
|
2679
|
+
let initializationCompletion: Promise<void> | undefined;
|
|
2680
|
+
let resolveInitialization: (() => void) | undefined;
|
|
2681
|
+
let initializationCompleted = false;
|
|
2682
|
+
const completeInitialization = () => {
|
|
2683
|
+
if (initializationCompleted || initializationCompletion === undefined) return;
|
|
2684
|
+
initializationCompleted = true;
|
|
2685
|
+
state.initializingSessions.delete(sessionId);
|
|
2686
|
+
if (state.sessionInitializations.get(sessionId) === initializationCompletion) {
|
|
2687
|
+
state.sessionInitializations.delete(sessionId);
|
|
2688
|
+
}
|
|
2689
|
+
resolveInitialization?.();
|
|
2690
|
+
};
|
|
2691
|
+
try {
|
|
2692
|
+
await this.mutateScope(state, () => {
|
|
2693
|
+
if (state.sessions.get(run.sessionId) !== run.stored || state.tombstones.has(run.sessionId)) {
|
|
2694
|
+
throw new FlexHarnessAbortError('The parent session no longer owns this subagent request.');
|
|
2695
|
+
}
|
|
2696
|
+
if (state.activeRuns.get(run.sessionId) !== run || run.callbacksClosed) {
|
|
2697
|
+
throw new FlexHarnessAbortError('The parent run no longer owns this subagent request.');
|
|
2698
|
+
}
|
|
2699
|
+
if (run.stored.session.agent !== undefined && !this.subagents.has(run.stored.session.agent)) {
|
|
2700
|
+
throw new FlexHarnessValidationError(`Parent subagent "${run.stored.session.agent}" is disabled.`);
|
|
2701
|
+
}
|
|
2702
|
+
if ((run.stored.session.depth ?? 0) >= this.maxSubagentDepth) {
|
|
2703
|
+
throw new FlexHarnessValidationError('The maximum subagent depth has been reached.');
|
|
2704
|
+
}
|
|
2705
|
+
let ancestor: IFlexSession | undefined = run.stored.session;
|
|
2706
|
+
const visited = new Set<string>();
|
|
2707
|
+
while (ancestor) {
|
|
2708
|
+
if (visited.has(ancestor.sessionId) || state.tombstones.has(ancestor.sessionId)) {
|
|
2709
|
+
throw new FlexHarnessValidationError('The subagent ancestor chain is invalid or deleted.');
|
|
2710
|
+
}
|
|
2711
|
+
visited.add(ancestor.sessionId);
|
|
2712
|
+
ancestor = ancestor.parentSessionId
|
|
2713
|
+
? state.sessions.get(ancestor.parentSessionId)?.session
|
|
2714
|
+
: undefined;
|
|
2715
|
+
}
|
|
2716
|
+
const existing = state.sessions.get(sessionId);
|
|
2717
|
+
if (existing) {
|
|
2718
|
+
if (
|
|
2719
|
+
existing.session.parentSessionId !== run.sessionId
|
|
2720
|
+
|| existing.session.agent !== definition.name
|
|
2721
|
+
) {
|
|
2722
|
+
throw new FlexHarnessValidationError(`Task session "${sessionId}" is not owned by this parent and agent.`);
|
|
2723
|
+
}
|
|
2724
|
+
if (
|
|
2725
|
+
taskId === undefined
|
|
2726
|
+
&& (existing.session.parentRunId !== run.runId
|
|
2727
|
+
|| existing.session.parentToolCallId !== toolCallId)
|
|
2728
|
+
) {
|
|
2729
|
+
throw new FlexHarnessValidationError(
|
|
2730
|
+
`Subagent task "${sessionId}" does not match its deterministic invocation origin.`,
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
if (state.initializingSessions.has(sessionId)) {
|
|
2734
|
+
throw new FlexHarnessSessionBusyError(sessionId, 'is still being initialized');
|
|
2735
|
+
}
|
|
2736
|
+
if (taskId === undefined && existing.messages.length > 0) {
|
|
2737
|
+
throw new FlexHarnessValidationError(
|
|
2738
|
+
`Subagent task "${sessionId}" has an uncertain prior execution and cannot be replayed automatically.`,
|
|
2739
|
+
);
|
|
2740
|
+
}
|
|
2741
|
+
if (state.activeRuns.has(sessionId) || existing.session.status !== 'idle') {
|
|
2742
|
+
throw new FlexHarnessSessionBusyError(sessionId, 'cannot be resumed while it is not idle');
|
|
2743
|
+
}
|
|
2744
|
+
if (taskId !== undefined && existing.session.parentRunId === run.runId) {
|
|
2745
|
+
throw new FlexHarnessValidationError('taskId can only resume a child from a later parent run.');
|
|
2746
|
+
}
|
|
2747
|
+
placeholder = existing;
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
if (taskId !== undefined || state.tombstones.has(sessionId)) {
|
|
2751
|
+
throw new FlexHarnessNotFoundError('Subagent task', sessionId);
|
|
2752
|
+
}
|
|
2753
|
+
const timestamp = new Date().toISOString();
|
|
2754
|
+
metadata = {
|
|
2755
|
+
scopeId: run.scopeId,
|
|
2756
|
+
sessionId,
|
|
2757
|
+
title: `Subagent: ${definition.name}`,
|
|
2758
|
+
createdAt: timestamp,
|
|
2759
|
+
updatedAt: timestamp,
|
|
2760
|
+
status: 'idle',
|
|
2761
|
+
activity: { status: 'idle' },
|
|
2762
|
+
parentSessionId: run.sessionId,
|
|
2763
|
+
parentRunId: run.runId,
|
|
2764
|
+
parentToolCallId: toolCallId,
|
|
2765
|
+
agent: definition.name,
|
|
2766
|
+
depth: (run.stored.session.depth ?? 0) + 1,
|
|
2767
|
+
};
|
|
2768
|
+
placeholder = this.createUninitializedStoredSession(metadata, state.storageKey);
|
|
2769
|
+
state.sessions.set(sessionId, placeholder);
|
|
2770
|
+
state.initializingSessions.add(sessionId);
|
|
2771
|
+
initializationCompletion = new Promise<void>((resolve) => {
|
|
2772
|
+
resolveInitialization = resolve;
|
|
2773
|
+
});
|
|
2774
|
+
state.sessionInitializations.set(sessionId, initializationCompletion);
|
|
2775
|
+
});
|
|
2776
|
+
if (!metadata) return { stored: placeholder!, created: false };
|
|
2777
|
+
const loaded = await this.loadSessionRuntime(
|
|
2778
|
+
state,
|
|
2779
|
+
metadata,
|
|
2780
|
+
run.scopeId,
|
|
2781
|
+
run.scope as TScope,
|
|
2782
|
+
placeholder!.compactorLifecycleController,
|
|
2783
|
+
);
|
|
2784
|
+
const parentLostOwnership = run.controller.signal.aborted
|
|
2785
|
+
|| state.sessions.get(run.sessionId) !== run.stored
|
|
2786
|
+
|| state.activeRuns.get(run.sessionId) !== run
|
|
2787
|
+
|| run.callbacksClosed;
|
|
2788
|
+
if (
|
|
2789
|
+
parentLostOwnership
|
|
2790
|
+
|| state.sessions.get(sessionId) !== placeholder
|
|
2791
|
+
|| state.tombstones.has(sessionId)
|
|
2792
|
+
) {
|
|
2793
|
+
const aborted = run.controller.signal.aborted
|
|
2794
|
+
? run.controller.signal.reason
|
|
2795
|
+
: new FlexHarnessAbortError('The subagent session lost parent ownership during initialization.');
|
|
2796
|
+
try {
|
|
2797
|
+
await this.closeStoredSession(loaded);
|
|
2798
|
+
} catch (error) {
|
|
2799
|
+
this.orphanedStoredSessions.add(loaded);
|
|
2800
|
+
throw combineErrors([aborted, error]);
|
|
2801
|
+
}
|
|
2802
|
+
throw aborted;
|
|
2803
|
+
}
|
|
2804
|
+
state.sessions.set(sessionId, loaded);
|
|
2805
|
+
this.emitEvent(run.scopeId, sessionId, {
|
|
2806
|
+
type: 'session.created',
|
|
2807
|
+
session: publicSnapshot(metadata),
|
|
2808
|
+
});
|
|
2809
|
+
return { stored: loaded, created: true };
|
|
2810
|
+
} catch (error) {
|
|
2811
|
+
if (metadata === undefined) throw error;
|
|
2812
|
+
if (
|
|
2813
|
+
state.tombstones.has(sessionId)
|
|
2814
|
+
&& placeholder?.compactorLifecycleController.signal.aborted
|
|
2815
|
+
) {
|
|
2816
|
+
throw placeholder.compactorLifecycleController.signal.reason;
|
|
2817
|
+
}
|
|
2818
|
+
const projected = this.projectExternalError(run, error, 'agentSession');
|
|
2819
|
+
if (
|
|
2820
|
+
placeholder === undefined
|
|
2821
|
+
|| (state.sessions.get(sessionId) !== placeholder && !state.tombstones.has(sessionId))
|
|
2822
|
+
) throw projected;
|
|
2823
|
+
if (state.tombstones.has(sessionId)) throw projected;
|
|
2824
|
+
try {
|
|
2825
|
+
await this.mutateScope(state, () => {
|
|
2826
|
+
if (state.sessions.get(sessionId) !== placeholder) return;
|
|
2827
|
+
state.sessions.delete(sessionId);
|
|
2828
|
+
state.tombstones.set(sessionId, {
|
|
2829
|
+
sessionId,
|
|
2830
|
+
deletedAt: new Date().toISOString(),
|
|
2831
|
+
rootSessionId: sessionId,
|
|
2832
|
+
depth: 0,
|
|
2833
|
+
parentSessionId: run.sessionId,
|
|
2834
|
+
});
|
|
2835
|
+
}, true);
|
|
2836
|
+
completeInitialization();
|
|
2837
|
+
await this.finishTombstoneCleanup(
|
|
2838
|
+
state,
|
|
2839
|
+
sessionId,
|
|
2840
|
+
run.scopeId,
|
|
2841
|
+
{ scopeId: run.scopeId, scope: run.scope as TScope },
|
|
2842
|
+
);
|
|
2843
|
+
} catch (cleanupError) {
|
|
2844
|
+
const combined = combineErrors([
|
|
2845
|
+
projected,
|
|
2846
|
+
this.projectExternalError(run, cleanupError, 'persistence'),
|
|
2847
|
+
]);
|
|
2848
|
+
this.deferNamespaceDrain(
|
|
2849
|
+
state,
|
|
2850
|
+
combined,
|
|
2851
|
+
{ scopeId: run.scopeId, scope: run.scope as TScope },
|
|
2852
|
+
);
|
|
2853
|
+
throw combined;
|
|
2854
|
+
}
|
|
2855
|
+
throw projected;
|
|
2856
|
+
} finally {
|
|
2857
|
+
completeInitialization();
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
private createSubagentSessionId(
|
|
2862
|
+
storageKey: string,
|
|
2863
|
+
parentSessionId: string,
|
|
2864
|
+
parentRunId: string,
|
|
2865
|
+
parentToolCallId: string,
|
|
2866
|
+
): string {
|
|
2867
|
+
return `subagent_${plugins.crypto.createHash('sha256').update(JSON.stringify([
|
|
2868
|
+
'flexharness-subagent-v1',
|
|
2869
|
+
storageKey,
|
|
2870
|
+
parentSessionId,
|
|
2871
|
+
parentRunId,
|
|
2872
|
+
parentToolCallId,
|
|
2873
|
+
])).digest('hex')}`;
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
private updateSubagentToolPart(
|
|
2877
|
+
run: IActiveRun,
|
|
2878
|
+
toolCallId: string,
|
|
2879
|
+
childSessionId: string,
|
|
2880
|
+
model?: IFlexModelIdentity,
|
|
2881
|
+
): void {
|
|
2882
|
+
const partId = run.toolPartIds.get(toolCallId);
|
|
2883
|
+
const part = run.callbackParts.find((entry) => entry.partId === partId && entry.type === 'tool');
|
|
2884
|
+
if (!part || part.type !== 'tool' || part.status !== 'running') {
|
|
2885
|
+
throw new FlexHarnessValidationError(`Running task part "${toolCallId}" is unavailable.`);
|
|
2886
|
+
}
|
|
2887
|
+
const bytes = Buffer.byteLength(childSessionId, 'utf8')
|
|
2888
|
+
+ (model === undefined ? 0 : jsonBytes(model));
|
|
2889
|
+
if (!this.reserveCallbackCapacity(run, 1, bytes, 0)) {
|
|
2890
|
+
throw run.callbackError ?? new FlexHarnessCallbackOverflowError('Task metadata exceeded callback limits.');
|
|
2891
|
+
}
|
|
2892
|
+
part.childSessionId = childSessionId;
|
|
2893
|
+
if (model !== undefined) part.model = cloneSerializable(model);
|
|
2894
|
+
this.emitPartEvent(run, 'part.updated', part);
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
private abortExactRun(
|
|
2898
|
+
state: IStorageState,
|
|
2899
|
+
sessionId: string,
|
|
2900
|
+
runId: string,
|
|
2901
|
+
reason: unknown,
|
|
2902
|
+
): boolean {
|
|
2903
|
+
const active = state.activeRuns.get(sessionId);
|
|
2904
|
+
if (!active || active.runId !== runId || active.phase === 'finalizing' || active.phase === 'promoting') {
|
|
2905
|
+
return false;
|
|
2906
|
+
}
|
|
2907
|
+
const cancellation = reason instanceof FlexHarnessAbortError
|
|
2908
|
+
? reason
|
|
2909
|
+
: this.trustInternalError(new FlexHarnessAbortError('The parent run was aborted.'));
|
|
2910
|
+
this.cancelRun(active, cancellation);
|
|
2911
|
+
return true;
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2171
2914
|
private createReservation(
|
|
2172
2915
|
run: IActiveRun,
|
|
2173
2916
|
prompt: INormalizedFlexPrompt,
|
|
@@ -2352,11 +3095,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2352
3095
|
private emitTerminalProjection(run: IActiveRun, terminal: IFlexTerminalProjection): void {
|
|
2353
3096
|
for (const part of terminal.assistantMessage.parts) {
|
|
2354
3097
|
const callbackPart = run.callbackParts.find((entry) => entry.partId === part.partId);
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
3098
|
+
const shouldComplete = part.type === 'text'
|
|
3099
|
+
? callbackPart?.type === 'text'
|
|
3100
|
+
: part.type === 'reasoning'
|
|
3101
|
+
? callbackPart?.type === 'reasoning' && callbackPart.status === 'running'
|
|
3102
|
+
: part.type === 'tool'
|
|
3103
|
+
? callbackPart?.type === 'tool' && callbackPart.status === 'running'
|
|
3104
|
+
: false;
|
|
3105
|
+
if (shouldComplete) {
|
|
2360
3106
|
this.emitPartEvent(run, 'part.completed', part);
|
|
2361
3107
|
}
|
|
2362
3108
|
}
|
|
@@ -2617,6 +3363,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2617
3363
|
state: IStorageState,
|
|
2618
3364
|
pending: IPendingPermission,
|
|
2619
3365
|
decision: TFlexPermissionDecision,
|
|
3366
|
+
context: IFlexAgentContextInvocation<TScope>,
|
|
2620
3367
|
): Promise<void> {
|
|
2621
3368
|
if (pending.settled) {
|
|
2622
3369
|
throw new FlexHarnessPermissionStateError(`Permission "${pending.request.permissionId}" has already been resolved.`);
|
|
@@ -2714,7 +3461,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2714
3461
|
const rejection = this.trustInternalError(
|
|
2715
3462
|
new FlexHarnessPermissionRejectedError(pending.request.permissionId),
|
|
2716
3463
|
);
|
|
2717
|
-
this.abortRunInternally(pending.run, rejection);
|
|
3464
|
+
this.abortRunInternally(pending.run, rejection, context);
|
|
2718
3465
|
pending.reject(rejection);
|
|
2719
3466
|
} else {
|
|
2720
3467
|
pending.resolve();
|
|
@@ -2818,19 +3565,50 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2818
3565
|
pending.reject(error);
|
|
2819
3566
|
}
|
|
2820
3567
|
|
|
2821
|
-
private cancelRun(
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
3568
|
+
private cancelRun(
|
|
3569
|
+
run: IActiveRun,
|
|
3570
|
+
reason: FlexHarnessAbortError,
|
|
3571
|
+
context?: IFlexAgentContextInvocation<TScope>,
|
|
3572
|
+
): void {
|
|
3573
|
+
const invocationContext = context ?? this.createRunCompactorContext(run);
|
|
3574
|
+
if (run.internalFailure === undefined && run.ownerCancellation === undefined) {
|
|
3575
|
+
run.ownerCancellation = reason;
|
|
3576
|
+
run.deferredCompactorContext = invocationContext;
|
|
3577
|
+
this.deferredCompactorContexts.set(run.originCompactorContext, invocationContext);
|
|
3578
|
+
}
|
|
3579
|
+
this.withCompactorContext(
|
|
3580
|
+
(run.deferredCompactorContext ?? invocationContext) as IFlexAgentContextInvocation<TScope>,
|
|
3581
|
+
() => {
|
|
3582
|
+
this.rejectRunPermissions(run.state, run, reason);
|
|
3583
|
+
if (!run.controller.signal.aborted) run.controller.abort(reason);
|
|
3584
|
+
if (run.phase === 'admitting') return;
|
|
3585
|
+
if (run.scheduleKey) {
|
|
3586
|
+
run.stored.agentSession.cancelScheduledGeneration(run.scheduleKey, reason);
|
|
3587
|
+
} else {
|
|
3588
|
+
run.stored.agentSession.abortCurrentGeneration(reason);
|
|
3589
|
+
}
|
|
3590
|
+
},
|
|
3591
|
+
);
|
|
2828
3592
|
}
|
|
2829
3593
|
|
|
2830
|
-
private abortRunInternally(
|
|
2831
|
-
run
|
|
2832
|
-
|
|
2833
|
-
|
|
3594
|
+
private abortRunInternally(
|
|
3595
|
+
run: IActiveRun,
|
|
3596
|
+
error: unknown,
|
|
3597
|
+
context?: IFlexAgentContextInvocation<TScope>,
|
|
3598
|
+
): void {
|
|
3599
|
+
const invocationContext = context ?? this.createRunCompactorContext(run);
|
|
3600
|
+
if (run.internalFailure === undefined) {
|
|
3601
|
+
run.internalFailure = error ?? new Error('Internal FlexHarness run failure.');
|
|
3602
|
+
run.deferredCompactorContext = invocationContext;
|
|
3603
|
+
this.deferredCompactorContexts.set(run.originCompactorContext, invocationContext);
|
|
3604
|
+
}
|
|
3605
|
+
this.withCompactorContext(
|
|
3606
|
+
(run.deferredCompactorContext ?? invocationContext) as IFlexAgentContextInvocation<TScope>,
|
|
3607
|
+
() => {
|
|
3608
|
+
if (!run.controller.signal.aborted) run.controller.abort(run.internalFailure);
|
|
3609
|
+
run.stored.agentSession.abortCurrentGeneration(run.internalFailure);
|
|
3610
|
+
},
|
|
3611
|
+
);
|
|
2834
3612
|
}
|
|
2835
3613
|
|
|
2836
3614
|
private async rollbackAdmission(
|
|
@@ -2841,7 +3619,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2841
3619
|
const errors: unknown[] = [];
|
|
2842
3620
|
if (run.transaction) {
|
|
2843
3621
|
try {
|
|
2844
|
-
await
|
|
3622
|
+
await this.withRunCompactorContext(
|
|
3623
|
+
run,
|
|
3624
|
+
() => run.stored.agentSession.finalizeGeneration(run.transaction!, 'interrupted'),
|
|
3625
|
+
);
|
|
2845
3626
|
} catch (error) {
|
|
2846
3627
|
errors.push(this.projectExternalError(run, error, 'agentSession'));
|
|
2847
3628
|
}
|
|
@@ -3068,6 +3849,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3068
3849
|
storageKey: string,
|
|
3069
3850
|
scopeId: string,
|
|
3070
3851
|
scope: TScope,
|
|
3852
|
+
compactorLifecycleController: AbortController,
|
|
3071
3853
|
): Promise<IStorageState> {
|
|
3072
3854
|
const snapshot = cloneSerializable(await this.stores.scopes.load(storageKey) ?? {
|
|
3073
3855
|
schemaVersion: 1,
|
|
@@ -3077,6 +3859,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3077
3859
|
} satisfies IFlexScopeSnapshot);
|
|
3078
3860
|
const state: IStorageState = {
|
|
3079
3861
|
storageKey,
|
|
3862
|
+
compactorLifecycleController,
|
|
3080
3863
|
scopeIdHint: scopeId,
|
|
3081
3864
|
revision: snapshot.revision,
|
|
3082
3865
|
sessions: new Map(),
|
|
@@ -3105,10 +3888,37 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3105
3888
|
scopeChanged = (await this.repairLoadedSession(state.storageKey, stored)) || scopeChanged;
|
|
3106
3889
|
if (stored.projectionRevision < 0) throw new Error('Invalid projection revision.');
|
|
3107
3890
|
}
|
|
3108
|
-
for (const
|
|
3891
|
+
for (const stored of state.sessions.values()) {
|
|
3892
|
+
let ancestorId = stored.session.parentSessionId;
|
|
3893
|
+
const visited = new Set<string>([stored.session.sessionId]);
|
|
3894
|
+
while (ancestorId) {
|
|
3895
|
+
if (visited.has(ancestorId)) {
|
|
3896
|
+
throw new FlexHarnessValidationError(
|
|
3897
|
+
`Live session "${stored.session.sessionId}" has a cyclic ancestor chain.`,
|
|
3898
|
+
);
|
|
3899
|
+
}
|
|
3900
|
+
visited.add(ancestorId);
|
|
3901
|
+
if (state.tombstones.has(ancestorId)) {
|
|
3902
|
+
throw new FlexHarnessValidationError(
|
|
3903
|
+
`Live session "${stored.session.sessionId}" descends from tombstoned ancestor "${ancestorId}".`,
|
|
3904
|
+
);
|
|
3905
|
+
}
|
|
3906
|
+
ancestorId = state.sessions.get(ancestorId)?.session.parentSessionId;
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
const tombstoneRoots = this.orderTombstoneRootsChildFirst(
|
|
3910
|
+
state,
|
|
3911
|
+
[...new Set([...state.tombstones.values()].map((tombstone) =>
|
|
3912
|
+
tombstone.rootSessionId ?? tombstone.sessionId))],
|
|
3913
|
+
);
|
|
3914
|
+
for (const rootSessionId of tombstoneRoots) {
|
|
3915
|
+
if (this.descendantTombstoneRoots(state, rootSessionId).length > 0) continue;
|
|
3109
3916
|
try {
|
|
3110
|
-
|
|
3111
|
-
|
|
3917
|
+
const group = this.tombstoneGroup(state, rootSessionId);
|
|
3918
|
+
for (const tombstone of group) {
|
|
3919
|
+
await this.cleanupSessionDomains(storageKey, tombstone.sessionId);
|
|
3920
|
+
}
|
|
3921
|
+
for (const tombstone of group) state.tombstones.delete(tombstone.sessionId);
|
|
3112
3922
|
scopeChanged = true;
|
|
3113
3923
|
} catch {
|
|
3114
3924
|
// A retained tombstone is retried by the next load or explicit delete call.
|
|
@@ -3141,6 +3951,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3141
3951
|
private createUninitializedStoredSession(
|
|
3142
3952
|
session: IFlexSession,
|
|
3143
3953
|
storageKey: string,
|
|
3954
|
+
compactorLifecycleController = new AbortController(),
|
|
3144
3955
|
): IStoredSessionState {
|
|
3145
3956
|
const unavailable = new Proxy({} as plugins.IAgentSession, {
|
|
3146
3957
|
get() {
|
|
@@ -3164,6 +3975,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3164
3975
|
agentEventStoreReleased: true,
|
|
3165
3976
|
executionContextCloseCompleted: true,
|
|
3166
3977
|
jobStoreReleased: true,
|
|
3978
|
+
compactorLifecycleController,
|
|
3167
3979
|
promptQueue: [],
|
|
3168
3980
|
outstandingPromptsById: new Map(),
|
|
3169
3981
|
terminalPromptQueueEntries: new Map(),
|
|
@@ -3176,8 +3988,15 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3176
3988
|
metadata: IFlexSession,
|
|
3177
3989
|
scopeId: string,
|
|
3178
3990
|
scope: TScope,
|
|
3991
|
+
compactorLifecycleController = new AbortController(),
|
|
3179
3992
|
): Promise<IStoredSessionState> {
|
|
3180
3993
|
const sessionId = metadata.sessionId;
|
|
3994
|
+
const compactorContext = this.createCompactorContext(
|
|
3995
|
+
scopeId,
|
|
3996
|
+
scope,
|
|
3997
|
+
state.storageKey,
|
|
3998
|
+
sessionId,
|
|
3999
|
+
);
|
|
3181
4000
|
const [projectionResult, permissionResult, eventStoreResult, jobStoreResult] = await Promise.allSettled([
|
|
3182
4001
|
this.stores.projections.load(state.storageKey, sessionId),
|
|
3183
4002
|
this.stores.permissions.load(state.storageKey, sessionId),
|
|
@@ -3216,13 +4035,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3216
4035
|
let executionContextHandle: IFlexExecutionContextHandle | undefined;
|
|
3217
4036
|
try {
|
|
3218
4037
|
if (this.executionContextProvider) {
|
|
3219
|
-
executionContextHandle = await this.
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
4038
|
+
executionContextHandle = await this.withCompactorContext(
|
|
4039
|
+
compactorContext,
|
|
4040
|
+
() => this.executionContextProvider!.provideExecutionContext({
|
|
4041
|
+
scopeId,
|
|
4042
|
+
scope,
|
|
4043
|
+
storageKey: state.storageKey,
|
|
4044
|
+
sessionId,
|
|
4045
|
+
jobStore,
|
|
4046
|
+
}),
|
|
4047
|
+
);
|
|
3226
4048
|
if (
|
|
3227
4049
|
executionContextHandle
|
|
3228
4050
|
&& (!executionContextHandle.context
|
|
@@ -3233,6 +4055,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3233
4055
|
}
|
|
3234
4056
|
const stored: IStoredSessionState = {
|
|
3235
4057
|
storageKey: state.storageKey,
|
|
4058
|
+
compactorContext,
|
|
3236
4059
|
session: cloneSerializable(metadata),
|
|
3237
4060
|
messages: cloneSerializable(projection?.messages ?? []),
|
|
3238
4061
|
stagedTerminals: cloneSerializable(projection?.stagedTerminals ?? []),
|
|
@@ -3250,21 +4073,75 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3250
4073
|
executionContextCloseCompleted: executionContextHandle?.close === undefined,
|
|
3251
4074
|
jobStoreReleased: this.stores.jobs.releaseSession === undefined,
|
|
3252
4075
|
jobs: executionContextHandle?.context.jobs,
|
|
4076
|
+
compactorLifecycleController,
|
|
3253
4077
|
promptQueue: [],
|
|
3254
4078
|
outstandingPromptsById: new Map(),
|
|
3255
4079
|
terminalPromptQueueEntries: new Map(),
|
|
3256
4080
|
outstandingPromptBytes: 0,
|
|
3257
4081
|
};
|
|
4082
|
+
const {
|
|
4083
|
+
contextCompactor,
|
|
4084
|
+
...agentSessionPolicy
|
|
4085
|
+
} = this.agentSessionPolicy;
|
|
4086
|
+
const executionContext = executionContextHandle?.context;
|
|
4087
|
+
const jobContext = executionContext?.jobs;
|
|
4088
|
+
const contextualExecutionContext = executionContext && jobContext?.subscribe
|
|
4089
|
+
? {
|
|
4090
|
+
...executionContext,
|
|
4091
|
+
jobs: {
|
|
4092
|
+
start: jobContext.start.bind(jobContext),
|
|
4093
|
+
get: jobContext.get.bind(jobContext),
|
|
4094
|
+
list: jobContext.list.bind(jobContext),
|
|
4095
|
+
...(jobContext.abort ? { abort: jobContext.abort.bind(jobContext) } : {}),
|
|
4096
|
+
subscribe: (listener: Parameters<NonNullable<typeof jobContext.subscribe>>[0]) =>
|
|
4097
|
+
jobContext.subscribe!((event) =>
|
|
4098
|
+
this.withCompactorContext(compactorContext, () => listener(event))),
|
|
4099
|
+
},
|
|
4100
|
+
}
|
|
4101
|
+
: executionContext;
|
|
3258
4102
|
const agentSessionOptions: plugins.IAgentSessionOptions & {
|
|
3259
4103
|
transactionOutcomeErrorProjector: (error: unknown) => string;
|
|
3260
4104
|
} = {
|
|
3261
|
-
...
|
|
4105
|
+
...agentSessionPolicy,
|
|
3262
4106
|
sessionId,
|
|
3263
4107
|
eventStore,
|
|
3264
|
-
executionContext:
|
|
4108
|
+
executionContext: contextualExecutionContext,
|
|
3265
4109
|
contextBuilder: ({ events }) => hydrateAgentMessages(
|
|
3266
4110
|
(this.agentSessionPolicy.contextBuilder ?? ((options) => plugins.buildModelMessages(options.events)))({ events }),
|
|
3267
4111
|
),
|
|
4112
|
+
...(contextCompactor
|
|
4113
|
+
? {
|
|
4114
|
+
contextCompactor: async (messages, events, options) => {
|
|
4115
|
+
const ambientContext = this.compactorInvocationContext.getStore();
|
|
4116
|
+
const invocationContext = ambientContext
|
|
4117
|
+
? (this.deferredCompactorContexts.get(ambientContext) ?? ambientContext) as
|
|
4118
|
+
IFlexAgentContextInvocation<TScope>
|
|
4119
|
+
: undefined;
|
|
4120
|
+
if (
|
|
4121
|
+
!invocationContext
|
|
4122
|
+
|| invocationContext.storageKey !== state.storageKey
|
|
4123
|
+
|| invocationContext.sessionId !== sessionId
|
|
4124
|
+
) {
|
|
4125
|
+
throw new Error('Agent context compaction is missing its exact FlexHarness invocation context.');
|
|
4126
|
+
}
|
|
4127
|
+
return contextCompactor(messages, events, {
|
|
4128
|
+
...options,
|
|
4129
|
+
...invocationContext,
|
|
4130
|
+
scope: invocationContext.scope as TScope,
|
|
4131
|
+
abortSignal: options.abortSignal
|
|
4132
|
+
? AbortSignal.any([
|
|
4133
|
+
options.abortSignal,
|
|
4134
|
+
state.compactorLifecycleController.signal,
|
|
4135
|
+
stored.compactorLifecycleController.signal,
|
|
4136
|
+
])
|
|
4137
|
+
: AbortSignal.any([
|
|
4138
|
+
state.compactorLifecycleController.signal,
|
|
4139
|
+
stored.compactorLifecycleController.signal,
|
|
4140
|
+
]),
|
|
4141
|
+
});
|
|
4142
|
+
},
|
|
4143
|
+
}
|
|
4144
|
+
: {}),
|
|
3268
4145
|
transactionOutcomeErrorProjector: () => 'The model operation failed.',
|
|
3269
4146
|
onToken: (delta) => this.onTextDelta(state, sessionId, delta),
|
|
3270
4147
|
onReasoningStart: (id) => this.onReasoningStart(state, sessionId, id),
|
|
@@ -3273,7 +4150,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3273
4150
|
onToolCallStart: (event) => this.onToolStart(state, sessionId, event),
|
|
3274
4151
|
onToolCallFinish: (event) => this.onToolFinish(state, sessionId, event),
|
|
3275
4152
|
};
|
|
3276
|
-
stored.agentSession = await
|
|
4153
|
+
stored.agentSession = await this.withCompactorContext(
|
|
4154
|
+
compactorContext,
|
|
4155
|
+
() => plugins.AgentSession.create(agentSessionOptions),
|
|
4156
|
+
);
|
|
3277
4157
|
return stored;
|
|
3278
4158
|
} catch (error) {
|
|
3279
4159
|
const cleanupErrors: unknown[] = [];
|
|
@@ -3740,38 +4620,136 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3740
4620
|
if (errors.length > 0) throw combineErrors(errors);
|
|
3741
4621
|
}
|
|
3742
4622
|
|
|
4623
|
+
private collectSessionSubtree(
|
|
4624
|
+
state: IStorageState,
|
|
4625
|
+
rootSessionId: string,
|
|
4626
|
+
): IStoredSessionState[] {
|
|
4627
|
+
const subtree: IStoredSessionState[] = [];
|
|
4628
|
+
const pending = [rootSessionId];
|
|
4629
|
+
const seen = new Set<string>();
|
|
4630
|
+
while (pending.length > 0) {
|
|
4631
|
+
const sessionId = pending.pop()!;
|
|
4632
|
+
if (seen.has(sessionId)) {
|
|
4633
|
+
throw new FlexHarnessValidationError('Session relationships contain a cycle.');
|
|
4634
|
+
}
|
|
4635
|
+
seen.add(sessionId);
|
|
4636
|
+
const stored = state.sessions.get(sessionId);
|
|
4637
|
+
if (!stored) continue;
|
|
4638
|
+
subtree.push(stored);
|
|
4639
|
+
const children = [...state.sessions.values()]
|
|
4640
|
+
.filter((candidate) => candidate.session.parentSessionId === sessionId)
|
|
4641
|
+
.map((candidate) => candidate.session.sessionId)
|
|
4642
|
+
.sort()
|
|
4643
|
+
.reverse();
|
|
4644
|
+
pending.push(...children);
|
|
4645
|
+
}
|
|
4646
|
+
return subtree;
|
|
4647
|
+
}
|
|
4648
|
+
|
|
4649
|
+
private tombstoneGroup(
|
|
4650
|
+
state: IStorageState,
|
|
4651
|
+
rootSessionId: string,
|
|
4652
|
+
): IFlexSessionTombstone[] {
|
|
4653
|
+
return [...state.tombstones.values()]
|
|
4654
|
+
.filter((tombstone) =>
|
|
4655
|
+
(tombstone.rootSessionId ?? tombstone.sessionId) === rootSessionId)
|
|
4656
|
+
.sort((left, right) =>
|
|
4657
|
+
(right.depth ?? 0) - (left.depth ?? 0)
|
|
4658
|
+
|| left.sessionId.localeCompare(right.sessionId));
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4661
|
+
private descendantTombstoneRoots(
|
|
4662
|
+
state: IStorageState,
|
|
4663
|
+
ancestorSessionId: string,
|
|
4664
|
+
): string[] {
|
|
4665
|
+
return [...new Set([...state.tombstones.values()]
|
|
4666
|
+
.map((tombstone) => tombstone.rootSessionId ?? tombstone.sessionId)
|
|
4667
|
+
.filter((rootSessionId) =>
|
|
4668
|
+
rootSessionId !== ancestorSessionId
|
|
4669
|
+
&& this.isSessionAncestor(state, ancestorSessionId, rootSessionId)))]
|
|
4670
|
+
.sort((left, right) =>
|
|
4671
|
+
this.sessionAncestryDepth(state, right) - this.sessionAncestryDepth(state, left)
|
|
4672
|
+
|| left.localeCompare(right));
|
|
4673
|
+
}
|
|
4674
|
+
|
|
4675
|
+
private orderTombstoneRootsChildFirst(
|
|
4676
|
+
state: IStorageState,
|
|
4677
|
+
rootSessionIds: readonly string[],
|
|
4678
|
+
): string[] {
|
|
4679
|
+
return [...rootSessionIds].sort((left, right) =>
|
|
4680
|
+
this.sessionAncestryDepth(state, right) - this.sessionAncestryDepth(state, left)
|
|
4681
|
+
|| left.localeCompare(right));
|
|
4682
|
+
}
|
|
4683
|
+
|
|
3743
4684
|
private finishTombstoneCleanup(
|
|
3744
4685
|
state: IStorageState,
|
|
3745
|
-
|
|
4686
|
+
rootSessionId: string,
|
|
3746
4687
|
scopeId: string,
|
|
4688
|
+
invocation?: { scopeId: string; scope: TScope },
|
|
3747
4689
|
): Promise<void> {
|
|
3748
|
-
const existing = state.tombstoneCleanups.get(
|
|
4690
|
+
const existing = state.tombstoneCleanups.get(rootSessionId);
|
|
3749
4691
|
if (existing) return existing;
|
|
3750
4692
|
let cleanup!: Promise<void>;
|
|
3751
|
-
cleanup = this.finishTombstoneCleanupInternal(
|
|
3752
|
-
|
|
3753
|
-
|
|
4693
|
+
cleanup = this.finishTombstoneCleanupInternal(
|
|
4694
|
+
state,
|
|
4695
|
+
rootSessionId,
|
|
4696
|
+
scopeId,
|
|
4697
|
+
invocation,
|
|
4698
|
+
).finally(() => {
|
|
4699
|
+
if (state.tombstoneCleanups.get(rootSessionId) === cleanup) {
|
|
4700
|
+
state.tombstoneCleanups.delete(rootSessionId);
|
|
3754
4701
|
}
|
|
3755
4702
|
});
|
|
3756
|
-
state.tombstoneCleanups.set(
|
|
4703
|
+
state.tombstoneCleanups.set(rootSessionId, cleanup);
|
|
3757
4704
|
return cleanup;
|
|
3758
4705
|
}
|
|
3759
4706
|
|
|
3760
4707
|
private async finishTombstoneCleanupInternal(
|
|
3761
4708
|
state: IStorageState,
|
|
3762
|
-
|
|
4709
|
+
rootSessionId: string,
|
|
3763
4710
|
scopeId: string,
|
|
4711
|
+
invocation?: { scopeId: string; scope: TScope },
|
|
3764
4712
|
): Promise<void> {
|
|
3765
|
-
const
|
|
3766
|
-
|
|
4713
|
+
for (const descendantRoot of this.descendantTombstoneRoots(state, rootSessionId)) {
|
|
4714
|
+
await this.finishTombstoneCleanup(state, descendantRoot, scopeId, invocation);
|
|
4715
|
+
}
|
|
4716
|
+
const group = this.tombstoneGroup(state, rootSessionId);
|
|
4717
|
+
if (group.length === 0) return;
|
|
4718
|
+
const groupIds = new Set(group.map((tombstone) => tombstone.sessionId));
|
|
4719
|
+
const reason = this.trustInternalError(new FlexHarnessAbortError('The session subtree was deleted.'));
|
|
4720
|
+
const contextFor = (
|
|
4721
|
+
sessionId: string,
|
|
4722
|
+
retained?: IRetainedSessionCleanup,
|
|
4723
|
+
): IFlexAgentContextInvocation<TScope> | undefined => invocation
|
|
4724
|
+
? this.createCompactorContext(
|
|
4725
|
+
invocation.scopeId,
|
|
4726
|
+
invocation.scope,
|
|
4727
|
+
state.storageKey,
|
|
4728
|
+
sessionId,
|
|
4729
|
+
)
|
|
4730
|
+
: retained?.stored.compactorContext as IFlexAgentContextInvocation<TScope> | undefined;
|
|
4731
|
+
for (const sessionId of groupIds) {
|
|
4732
|
+
const retained = state.retainedSessionCleanups.get(sessionId);
|
|
4733
|
+
const context = contextFor(sessionId, retained);
|
|
4734
|
+
if (retained) {
|
|
4735
|
+
this.abortCompactorLifecycle(retained.stored, reason);
|
|
4736
|
+
this.cancelStoredPromptQueue(retained.stored, reason, undefined, context);
|
|
4737
|
+
}
|
|
4738
|
+
const run = state.activeRuns.get(sessionId);
|
|
4739
|
+
if (run) this.cancelRun(run, reason, context);
|
|
4740
|
+
}
|
|
4741
|
+
await Promise.allSettled([...groupIds]
|
|
4742
|
+
.map((sessionId) => state.sessionInitializations.get(sessionId))
|
|
4743
|
+
.filter((completion): completion is Promise<void> => completion !== undefined));
|
|
4744
|
+
for (const tombstone of group) {
|
|
4745
|
+
const sessionId = tombstone.sessionId;
|
|
4746
|
+
const retained = state.retainedSessionCleanups.get(sessionId);
|
|
3767
4747
|
const errors: unknown[] = [];
|
|
3768
|
-
const reason = this.trustInternalError(new FlexHarnessAbortError('The session was deleted.'));
|
|
3769
|
-
this.cancelStoredPromptQueue(retained.stored, reason);
|
|
3770
4748
|
const run = state.activeRuns.get(sessionId);
|
|
3771
|
-
|
|
3772
|
-
if (!retained.stored.agentSessionAbortCompleted) {
|
|
4749
|
+
const context = contextFor(sessionId, retained);
|
|
4750
|
+
if (retained && !retained.stored.agentSessionAbortCompleted) {
|
|
3773
4751
|
try {
|
|
3774
|
-
await this.abortStoredSession(retained.stored, reason);
|
|
4752
|
+
await this.abortStoredSession(retained.stored, reason, context);
|
|
3775
4753
|
} catch (error) {
|
|
3776
4754
|
errors.push(this.projectOperationError(
|
|
3777
4755
|
error,
|
|
@@ -3784,70 +4762,135 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3784
4762
|
}
|
|
3785
4763
|
if (run) {
|
|
3786
4764
|
const settled = await Promise.allSettled([run.completion]);
|
|
3787
|
-
if (settled[0].status === 'rejected'
|
|
3788
|
-
|
|
4765
|
+
if (settled[0].status === 'rejected') {
|
|
4766
|
+
this.appendUnexpectedErrors(errors, settled[0].reason);
|
|
3789
4767
|
}
|
|
3790
4768
|
}
|
|
3791
|
-
if (retained
|
|
3792
|
-
|
|
3793
|
-
|
|
4769
|
+
if (retained) {
|
|
4770
|
+
if (retained.stored.promptQueueDrain) {
|
|
4771
|
+
const settled = await Promise.allSettled([retained.stored.promptQueueDrain]);
|
|
4772
|
+
if (settled[0].status === 'rejected') errors.push(settled[0].reason);
|
|
4773
|
+
}
|
|
4774
|
+
try {
|
|
4775
|
+
await this.closeStoredSession(retained.stored, context);
|
|
4776
|
+
} catch (error) {
|
|
4777
|
+
this.appendUnexpectedErrors(errors, this.projectOperationError(
|
|
4778
|
+
error,
|
|
4779
|
+
'toolCleanup',
|
|
4780
|
+
scopeId,
|
|
4781
|
+
sessionId,
|
|
4782
|
+
'session-delete',
|
|
4783
|
+
));
|
|
4784
|
+
}
|
|
3794
4785
|
}
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
sessionId,
|
|
3803
|
-
'session-delete',
|
|
3804
|
-
));
|
|
4786
|
+
if (errors.length > 0) {
|
|
4787
|
+
if (
|
|
4788
|
+
retained
|
|
4789
|
+
&& state.lifecycle === 'retired'
|
|
4790
|
+
&& !this.storedSessionCleanupCompleted(retained.stored)
|
|
4791
|
+
) this.orphanedStoredSessions.add(retained.stored);
|
|
4792
|
+
throw combineErrors(errors);
|
|
3805
4793
|
}
|
|
3806
|
-
if (
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
4794
|
+
if (retained) this.purgeStoredPromptQueue(retained.stored);
|
|
4795
|
+
if (!retained?.domainsCompleted) {
|
|
4796
|
+
if (this.hasOrphanedSessionResources(state.storageKey, sessionId)) {
|
|
4797
|
+
throw new Error(`Session "${sessionId}" still has runtime cleanup ownership.`);
|
|
4798
|
+
}
|
|
4799
|
+
await this.cleanupSessionDomains(state.storageKey, sessionId);
|
|
4800
|
+
if (retained) retained.domainsCompleted = true;
|
|
3812
4801
|
}
|
|
3813
|
-
await this.cleanupSessionDomains(state.storageKey, sessionId);
|
|
3814
|
-
if (retained) retained.domainsCompleted = true;
|
|
3815
4802
|
}
|
|
3816
4803
|
await this.mutateScope(state, () => {
|
|
3817
|
-
|
|
3818
|
-
|
|
4804
|
+
for (const tombstone of group) {
|
|
4805
|
+
state.tombstones.delete(tombstone.sessionId);
|
|
4806
|
+
state.retainedSessionCleanups.delete(tombstone.sessionId);
|
|
4807
|
+
}
|
|
3819
4808
|
}, true);
|
|
3820
4809
|
}
|
|
3821
4810
|
|
|
3822
4811
|
private async fenceNamespace(state: IStorageState, currentRun: IActiveRun, cause: unknown): Promise<void> {
|
|
3823
4812
|
if (state.lifecycle === 'retired') return;
|
|
4813
|
+
if (this.storageDrains.has(state.storageKey)) {
|
|
4814
|
+
state.fenceAdditionalErrors.push(cause);
|
|
4815
|
+
return;
|
|
4816
|
+
}
|
|
3824
4817
|
if (state.fenceInProgress) {
|
|
3825
4818
|
state.fenceAdditionalErrors.push(cause);
|
|
3826
4819
|
return;
|
|
3827
4820
|
}
|
|
3828
4821
|
state.fenceInProgress = true;
|
|
3829
4822
|
state.lifecycle = 'fenced';
|
|
3830
|
-
const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
|
|
3831
4823
|
const reason = this.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.'));
|
|
4824
|
+
if (!state.compactorLifecycleController.signal.aborted) {
|
|
4825
|
+
state.compactorLifecycleController.abort(reason);
|
|
4826
|
+
}
|
|
3832
4827
|
const cleanupErrors: unknown[] = [];
|
|
4828
|
+
const ambientContext = this.compactorInvocationContext.getStore();
|
|
4829
|
+
const invocation = ambientContext?.storageKey === state.storageKey
|
|
4830
|
+
&& ambientContext.sessionId === currentRun.sessionId
|
|
4831
|
+
? ambientContext
|
|
4832
|
+
: this.effectiveRunCompactorContext(currentRun);
|
|
4833
|
+
const contextFor = (sessionId: string) => this.createCompactorContext(
|
|
4834
|
+
invocation.scopeId,
|
|
4835
|
+
invocation.scope,
|
|
4836
|
+
state.storageKey,
|
|
4837
|
+
sessionId,
|
|
4838
|
+
);
|
|
3833
4839
|
try {
|
|
3834
4840
|
for (const stored of state.sessions.values()) {
|
|
4841
|
+
this.abortCompactorLifecycle(stored, reason);
|
|
3835
4842
|
this.cancelStoredPromptQueue(
|
|
3836
4843
|
stored,
|
|
3837
4844
|
reason,
|
|
3838
4845
|
stored === currentRun.stored ? currentRun.queueId : undefined,
|
|
4846
|
+
contextFor(stored.session.sessionId),
|
|
3839
4847
|
);
|
|
3840
4848
|
}
|
|
3841
4849
|
const otherRuns = [...state.activeRuns.values()].filter((run) => run !== currentRun);
|
|
3842
|
-
for (const run of otherRuns) this.cancelRun(run, reason);
|
|
3843
|
-
const
|
|
4850
|
+
for (const run of otherRuns) this.cancelRun(run, reason, contextFor(run.sessionId));
|
|
4851
|
+
const dependentRuns = otherRuns.filter((run) =>
|
|
4852
|
+
this.sessionsAreDependencyRelated(state, currentRun.sessionId, run.sessionId));
|
|
4853
|
+
const independentRuns = otherRuns.filter((run) => !dependentRuns.includes(run));
|
|
4854
|
+
const runResults = await Promise.allSettled(independentRuns.map((run) => run.completion));
|
|
3844
4855
|
for (const result of runResults) {
|
|
3845
4856
|
if (result.status === 'rejected') this.appendUnexpectedErrors(cleanupErrors, result.reason);
|
|
3846
4857
|
}
|
|
4858
|
+
if (dependentRuns.length > 0) {
|
|
4859
|
+
state.fenceAdditionalErrors.push(cause, ...cleanupErrors);
|
|
4860
|
+
const stateLoad = this.stateLoads.get(state.storageKey);
|
|
4861
|
+
if (!stateLoad) {
|
|
4862
|
+
throw combineErrors([cause, ...cleanupErrors, new Error(
|
|
4863
|
+
'The fenced namespace no longer has durable cleanup ownership.',
|
|
4864
|
+
)]);
|
|
4865
|
+
}
|
|
4866
|
+
const deferredDrain = this.drainStorage(
|
|
4867
|
+
state.storageKey,
|
|
4868
|
+
stateLoad,
|
|
4869
|
+
reason,
|
|
4870
|
+
{ scopeId: invocation.scopeId, scope: invocation.scope },
|
|
4871
|
+
);
|
|
4872
|
+
void deferredDrain.catch(() => undefined);
|
|
4873
|
+
return;
|
|
4874
|
+
}
|
|
4875
|
+
const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
|
|
3847
4876
|
await Promise.allSettled([...state.sessionInitializations.values()]);
|
|
3848
4877
|
cleanupErrors.push(...await this.closeOrphanedResources(state.storageKey));
|
|
3849
4878
|
await state.scopeQueue;
|
|
3850
|
-
const
|
|
4879
|
+
const currentTombstoneRoot = state.tombstones.get(currentRun.sessionId)?.rootSessionId
|
|
4880
|
+
?? currentRun.sessionId;
|
|
4881
|
+
const currentTombstoneCleanup = state.tombstoneCleanups.get(currentTombstoneRoot);
|
|
4882
|
+
const currentTombstoneSessions = new Set(
|
|
4883
|
+
this.tombstoneGroup(state, currentTombstoneRoot).map((tombstone) => tombstone.sessionId),
|
|
4884
|
+
);
|
|
4885
|
+
if (currentTombstoneCleanup) {
|
|
4886
|
+
this.retainOrphanedTombstoneCleanup(
|
|
4887
|
+
state.storageKey,
|
|
4888
|
+
currentTombstoneRoot,
|
|
4889
|
+
currentTombstoneCleanup,
|
|
4890
|
+
);
|
|
4891
|
+
}
|
|
4892
|
+
const tombstoneAttempts = [...state.tombstoneCleanups.entries()]
|
|
4893
|
+
.filter(([rootSessionId]) => rootSessionId !== currentTombstoneRoot);
|
|
3851
4894
|
const tombstoneResults = await Promise.allSettled(
|
|
3852
4895
|
tombstoneAttempts.map(([, cleanup]) => cleanup),
|
|
3853
4896
|
);
|
|
@@ -3855,15 +4898,19 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3855
4898
|
if (result.status === 'rejected') this.appendUnexpectedErrors(cleanupErrors, result.reason);
|
|
3856
4899
|
}
|
|
3857
4900
|
const attemptedTombstones = new Set(tombstoneAttempts.map(([sessionId]) => sessionId));
|
|
4901
|
+
const attemptedTombstoneSessions = new Set([...attemptedTombstones].flatMap((rootSessionId) =>
|
|
4902
|
+
this.tombstoneGroup(state, rootSessionId).map((tombstone) => tombstone.sessionId)));
|
|
3858
4903
|
const storedSessions = new Set([
|
|
3859
4904
|
...state.sessions.values(),
|
|
3860
4905
|
...[...state.retainedSessionCleanups]
|
|
3861
|
-
.filter(([sessionId]) =>
|
|
4906
|
+
.filter(([sessionId]) =>
|
|
4907
|
+
!currentTombstoneSessions.has(sessionId)
|
|
4908
|
+
&& !attemptedTombstoneSessions.has(sessionId))
|
|
3862
4909
|
.map(([, retained]) => retained.stored),
|
|
3863
4910
|
]);
|
|
3864
4911
|
for (const stored of storedSessions) {
|
|
3865
4912
|
try {
|
|
3866
|
-
await this.abortStoredSession(stored, reason);
|
|
4913
|
+
await this.abortStoredSession(stored, reason, contextFor(stored.session.sessionId));
|
|
3867
4914
|
} catch (error) {
|
|
3868
4915
|
cleanupErrors.push(this.projectOperationError(
|
|
3869
4916
|
error,
|
|
@@ -3874,7 +4921,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3874
4921
|
));
|
|
3875
4922
|
}
|
|
3876
4923
|
try {
|
|
3877
|
-
await this.closeStoredSession(stored);
|
|
4924
|
+
await this.closeStoredSession(stored, contextFor(stored.session.sessionId));
|
|
3878
4925
|
} catch (error) {
|
|
3879
4926
|
if (!this.storedSessionCleanupCompleted(stored)) {
|
|
3880
4927
|
cleanupErrors.push(this.projectOperationError(
|
|
@@ -3893,24 +4940,111 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3893
4940
|
if (cleanupErrors.length > 0) throw combineErrors([cause, ...cleanupErrors]);
|
|
3894
4941
|
state.lifecycle = 'retired';
|
|
3895
4942
|
state.sessions.clear();
|
|
4943
|
+
const currentRetainedCleanups = [...state.retainedSessionCleanups]
|
|
4944
|
+
.filter(([sessionId]) => currentTombstoneSessions.has(sessionId));
|
|
3896
4945
|
state.retainedSessionCleanups.clear();
|
|
4946
|
+
for (const [sessionId, retained] of currentRetainedCleanups) {
|
|
4947
|
+
state.retainedSessionCleanups.set(sessionId, retained);
|
|
4948
|
+
}
|
|
3897
4949
|
state.sessionDeletions.clear();
|
|
3898
4950
|
state.tombstoneCleanups.clear();
|
|
4951
|
+
if (currentTombstoneCleanup) {
|
|
4952
|
+
state.tombstoneCleanups.set(currentTombstoneRoot, currentTombstoneCleanup);
|
|
4953
|
+
}
|
|
3899
4954
|
state.activeRuns.clear();
|
|
3900
4955
|
state.pendingPermissions.clear();
|
|
3901
4956
|
state.initializingSessions.clear();
|
|
3902
4957
|
state.sessionInitializations.clear();
|
|
3903
4958
|
this.stateLoads.delete(state.storageKey);
|
|
4959
|
+
if (
|
|
4960
|
+
this.storageCompactorLifecycleControllers.get(state.storageKey)
|
|
4961
|
+
=== state.compactorLifecycleController
|
|
4962
|
+
) this.storageCompactorLifecycleControllers.delete(state.storageKey);
|
|
3904
4963
|
} finally {
|
|
3905
4964
|
state.fenceInProgress = false;
|
|
3906
4965
|
}
|
|
3907
4966
|
}
|
|
3908
4967
|
|
|
3909
|
-
private
|
|
4968
|
+
private deferNamespaceDrain(
|
|
4969
|
+
state: IStorageState,
|
|
4970
|
+
cause: unknown,
|
|
4971
|
+
invocation?: { scopeId: string; scope: TScope },
|
|
4972
|
+
): void {
|
|
4973
|
+
state.lifecycle = 'fenced';
|
|
4974
|
+
state.fenceAdditionalErrors.push(cause);
|
|
4975
|
+
const stateLoad = this.stateLoads.get(state.storageKey);
|
|
4976
|
+
if (!stateLoad) return;
|
|
4977
|
+
const drain = this.drainStorage(
|
|
4978
|
+
state.storageKey,
|
|
4979
|
+
stateLoad,
|
|
4980
|
+
this.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.')),
|
|
4981
|
+
invocation,
|
|
4982
|
+
);
|
|
4983
|
+
void drain.catch(() => undefined);
|
|
4984
|
+
}
|
|
4985
|
+
|
|
4986
|
+
private sessionsAreDependencyRelated(
|
|
4987
|
+
state: IStorageState,
|
|
4988
|
+
leftSessionId: string,
|
|
4989
|
+
rightSessionId: string,
|
|
4990
|
+
): boolean {
|
|
4991
|
+
return this.isSessionAncestor(state, leftSessionId, rightSessionId)
|
|
4992
|
+
|| this.isSessionAncestor(state, rightSessionId, leftSessionId);
|
|
4993
|
+
}
|
|
4994
|
+
|
|
4995
|
+
private isSessionAncestor(
|
|
4996
|
+
state: IStorageState,
|
|
4997
|
+
ancestorSessionId: string,
|
|
4998
|
+
descendantSessionId: string,
|
|
4999
|
+
): boolean {
|
|
5000
|
+
const visited = new Set<string>();
|
|
5001
|
+
let currentId: string | undefined = descendantSessionId;
|
|
5002
|
+
while (currentId) {
|
|
5003
|
+
if (visited.has(currentId)) return false;
|
|
5004
|
+
visited.add(currentId);
|
|
5005
|
+
const parentSessionId = this.sessionParentSessionId(state, currentId);
|
|
5006
|
+
if (parentSessionId === ancestorSessionId) return true;
|
|
5007
|
+
currentId = parentSessionId;
|
|
5008
|
+
}
|
|
5009
|
+
return false;
|
|
5010
|
+
}
|
|
5011
|
+
|
|
5012
|
+
private sessionAncestryDepth(state: IStorageState, sessionId: string): number {
|
|
5013
|
+
const visited = new Set<string>();
|
|
5014
|
+
let currentId: string | undefined = sessionId;
|
|
5015
|
+
let depth = 0;
|
|
5016
|
+
while (currentId) {
|
|
5017
|
+
if (visited.has(currentId)) return depth;
|
|
5018
|
+
visited.add(currentId);
|
|
5019
|
+
const parentSessionId = this.sessionParentSessionId(state, currentId);
|
|
5020
|
+
if (!parentSessionId) return depth;
|
|
5021
|
+
depth++;
|
|
5022
|
+
currentId = parentSessionId;
|
|
5023
|
+
}
|
|
5024
|
+
return depth;
|
|
5025
|
+
}
|
|
5026
|
+
|
|
5027
|
+
private sessionParentSessionId(state: IStorageState, sessionId: string): string | undefined {
|
|
5028
|
+
return this.sessionMetadata(state, sessionId)?.parentSessionId
|
|
5029
|
+
?? state.tombstones.get(sessionId)?.parentSessionId;
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
private sessionMetadata(state: IStorageState, sessionId: string): IFlexSession | undefined {
|
|
5033
|
+
return state.sessions.get(sessionId)?.session
|
|
5034
|
+
?? state.retainedSessionCleanups.get(sessionId)?.stored.session;
|
|
5035
|
+
}
|
|
5036
|
+
|
|
5037
|
+
private async closeStoredSession(
|
|
5038
|
+
stored: IStoredSessionState,
|
|
5039
|
+
context: IFlexAgentContextInvocation<unknown> = stored.compactorContext!,
|
|
5040
|
+
): Promise<void> {
|
|
3910
5041
|
const errors: unknown[] = [];
|
|
3911
5042
|
if (!stored.agentSessionCloseCompleted) {
|
|
3912
5043
|
try {
|
|
3913
|
-
await
|
|
5044
|
+
await this.withCompactorContext(
|
|
5045
|
+
context,
|
|
5046
|
+
() => stored.agentSession.close(),
|
|
5047
|
+
);
|
|
3914
5048
|
if (!stored.agentSession.closeCleanupCompleted) {
|
|
3915
5049
|
throw new Error('AgentSession.close() resolved before cleanup ownership was released.');
|
|
3916
5050
|
}
|
|
@@ -3960,9 +5094,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3960
5094
|
private async abortStoredSession(
|
|
3961
5095
|
stored: IStoredSessionState,
|
|
3962
5096
|
reason: FlexHarnessAbortError,
|
|
5097
|
+
context: IFlexAgentContextInvocation<unknown> = stored.compactorContext!,
|
|
3963
5098
|
): Promise<void> {
|
|
3964
5099
|
if (stored.agentSessionAbortCompleted) return;
|
|
3965
|
-
await
|
|
5100
|
+
await this.withCompactorContext(
|
|
5101
|
+
context,
|
|
5102
|
+
() => stored.agentSession.abortSession(reason, { abortBackgroundJobs: true }),
|
|
5103
|
+
);
|
|
3966
5104
|
stored.agentSessionAbortCompleted = true;
|
|
3967
5105
|
}
|
|
3968
5106
|
|
|
@@ -4018,6 +5156,29 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4018
5156
|
retained.storageKey === storageKey && retained.sessionId === sessionId);
|
|
4019
5157
|
}
|
|
4020
5158
|
|
|
5159
|
+
private retainOrphanedTombstoneCleanup(
|
|
5160
|
+
storageKey: string,
|
|
5161
|
+
sessionId: string,
|
|
5162
|
+
completion: Promise<void>,
|
|
5163
|
+
): void {
|
|
5164
|
+
const key = JSON.stringify([storageKey, sessionId]);
|
|
5165
|
+
if (this.orphanedTombstoneCleanups.has(key)) return;
|
|
5166
|
+
const retained: IOrphanedTombstoneCleanup = { storageKey, sessionId, completion };
|
|
5167
|
+
this.orphanedTombstoneCleanups.set(key, retained);
|
|
5168
|
+
void completion.then(
|
|
5169
|
+
() => {
|
|
5170
|
+
if (this.orphanedTombstoneCleanups.get(key) === retained) {
|
|
5171
|
+
this.orphanedTombstoneCleanups.delete(key);
|
|
5172
|
+
}
|
|
5173
|
+
},
|
|
5174
|
+
() => {
|
|
5175
|
+
if (this.orphanedTombstoneCleanups.get(key) === retained) {
|
|
5176
|
+
this.orphanedTombstoneCleanups.delete(key);
|
|
5177
|
+
}
|
|
5178
|
+
},
|
|
5179
|
+
);
|
|
5180
|
+
}
|
|
5181
|
+
|
|
4021
5182
|
private closeOrphanedResources(storageKey?: string): Promise<unknown[]> {
|
|
4022
5183
|
const operation = this.orphanedResourceQueue.then(() =>
|
|
4023
5184
|
this.closeOrphanedResourcesInternal(storageKey));
|
|
@@ -4027,6 +5188,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4027
5188
|
|
|
4028
5189
|
private async closeOrphanedResourcesInternal(storageKey?: string): Promise<unknown[]> {
|
|
4029
5190
|
const errors: unknown[] = [];
|
|
5191
|
+
const tombstoneCleanups = [...this.orphanedTombstoneCleanups.values()]
|
|
5192
|
+
.filter((retained) => storageKey === undefined || retained.storageKey === storageKey);
|
|
5193
|
+
const tombstoneResults = await Promise.allSettled(
|
|
5194
|
+
tombstoneCleanups.map((retained) => retained.completion),
|
|
5195
|
+
);
|
|
5196
|
+
for (const result of tombstoneResults) {
|
|
5197
|
+
if (result.status === 'rejected') this.appendUnexpectedErrors(errors, result.reason);
|
|
5198
|
+
}
|
|
4030
5199
|
for (const stored of [...this.orphanedStoredSessions]) {
|
|
4031
5200
|
if (storageKey !== undefined && stored.storageKey !== storageKey) continue;
|
|
4032
5201
|
try {
|
|
@@ -4067,6 +5236,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4067
5236
|
...[...this.orphanedStoredSessions].map((stored) => stored.storageKey),
|
|
4068
5237
|
...[...this.orphanedExecutionContexts.values()].map((owner) => owner.storageKey),
|
|
4069
5238
|
...[...this.orphanedProviderReleases.values()].map((retained) => retained.storageKey),
|
|
5239
|
+
...[...this.orphanedTombstoneCleanups.values()].map((retained) => retained.storageKey),
|
|
4070
5240
|
]);
|
|
4071
5241
|
}
|
|
4072
5242
|
|
|
@@ -4089,7 +5259,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4089
5259
|
}
|
|
4090
5260
|
} else {
|
|
4091
5261
|
try {
|
|
4092
|
-
await this.drainStorage(
|
|
5262
|
+
await this.drainStorage(
|
|
5263
|
+
scope.storageKey,
|
|
5264
|
+
stateLoad,
|
|
5265
|
+
this.createScopeRetirementError(),
|
|
5266
|
+
{ scopeId, scope: scope.scope },
|
|
5267
|
+
);
|
|
4093
5268
|
} catch (error) {
|
|
4094
5269
|
this.appendUnexpectedErrors(errors, error);
|
|
4095
5270
|
}
|
|
@@ -4101,15 +5276,25 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4101
5276
|
storageKey: string,
|
|
4102
5277
|
stateLoad: Promise<IStorageState>,
|
|
4103
5278
|
reason: FlexHarnessAbortError,
|
|
5279
|
+
invocation?: { scopeId: string; scope: TScope },
|
|
4104
5280
|
): Promise<void> {
|
|
5281
|
+
const compactorController = this.storageCompactorLifecycleControllers.get(storageKey);
|
|
5282
|
+
if (compactorController && !compactorController.signal.aborted) {
|
|
5283
|
+
compactorController.abort(reason);
|
|
5284
|
+
}
|
|
4105
5285
|
const existing = this.storageDrains.get(storageKey);
|
|
4106
5286
|
if (existing) return existing;
|
|
4107
5287
|
let completed = false;
|
|
4108
5288
|
let drain!: Promise<void>;
|
|
4109
|
-
drain = this.drainStorageInternal(storageKey, stateLoad, reason).then(() => {
|
|
5289
|
+
drain = this.drainStorageInternal(storageKey, stateLoad, reason, invocation).then(() => {
|
|
4110
5290
|
completed = true;
|
|
4111
5291
|
}).finally(() => {
|
|
4112
|
-
if (completed && this.stateLoads.get(storageKey) === stateLoad)
|
|
5292
|
+
if (completed && this.stateLoads.get(storageKey) === stateLoad) {
|
|
5293
|
+
this.stateLoads.delete(storageKey);
|
|
5294
|
+
if (this.storageCompactorLifecycleControllers.get(storageKey) === compactorController) {
|
|
5295
|
+
this.storageCompactorLifecycleControllers.delete(storageKey);
|
|
5296
|
+
}
|
|
5297
|
+
}
|
|
4113
5298
|
if (this.storageDrains.get(storageKey) === drain) this.storageDrains.delete(storageKey);
|
|
4114
5299
|
});
|
|
4115
5300
|
this.storageDrains.set(storageKey, drain);
|
|
@@ -4120,6 +5305,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4120
5305
|
storageKey: string,
|
|
4121
5306
|
stateLoad: Promise<IStorageState>,
|
|
4122
5307
|
reason: FlexHarnessAbortError,
|
|
5308
|
+
invocation?: { scopeId: string; scope: TScope },
|
|
4123
5309
|
): Promise<void> {
|
|
4124
5310
|
let state: IStorageState;
|
|
4125
5311
|
try {
|
|
@@ -4132,14 +5318,35 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4132
5318
|
if (state.lifecycle !== 'fenced') state.lifecycle = 'retiring';
|
|
4133
5319
|
const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
|
|
4134
5320
|
const errors: unknown[] = [];
|
|
4135
|
-
|
|
5321
|
+
const contextFor = (sessionId: string) => invocation
|
|
5322
|
+
? this.createCompactorContext(invocation.scopeId, invocation.scope, storageKey, sessionId)
|
|
5323
|
+
: undefined;
|
|
5324
|
+
for (const stored of state.sessions.values()) {
|
|
5325
|
+
this.abortCompactorLifecycle(stored, reason);
|
|
5326
|
+
this.cancelStoredPromptQueue(
|
|
5327
|
+
stored,
|
|
5328
|
+
reason,
|
|
5329
|
+
undefined,
|
|
5330
|
+
contextFor(stored.session.sessionId),
|
|
5331
|
+
);
|
|
5332
|
+
}
|
|
4136
5333
|
for (const run of state.activeRuns.values()) {
|
|
4137
|
-
if (run.phase !== 'finalizing' && run.phase !== 'promoting')
|
|
5334
|
+
if (run.phase !== 'finalizing' && run.phase !== 'promoting') {
|
|
5335
|
+
this.cancelRun(run, reason, contextFor(run.sessionId));
|
|
5336
|
+
}
|
|
4138
5337
|
}
|
|
4139
5338
|
for (const pending of [...state.pendingPermissions.values()]) this.rejectPending(state, pending, reason);
|
|
5339
|
+
const runResults = await Promise.allSettled([...state.activeRuns.values()].map((run) => run.completion));
|
|
5340
|
+
for (const result of runResults) {
|
|
5341
|
+
if (result.status === 'rejected') this.appendUnexpectedErrors(errors, result.reason);
|
|
5342
|
+
}
|
|
4140
5343
|
for (const stored of state.sessions.values()) {
|
|
4141
5344
|
try {
|
|
4142
|
-
await this.abortStoredSession(
|
|
5345
|
+
await this.abortStoredSession(
|
|
5346
|
+
stored,
|
|
5347
|
+
reason,
|
|
5348
|
+
contextFor(stored.session.sessionId),
|
|
5349
|
+
);
|
|
4143
5350
|
} catch (error) {
|
|
4144
5351
|
errors.push(this.projectOperationError(
|
|
4145
5352
|
error,
|
|
@@ -4150,10 +5357,6 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4150
5357
|
));
|
|
4151
5358
|
}
|
|
4152
5359
|
}
|
|
4153
|
-
const runResults = await Promise.allSettled([...state.activeRuns.values()].map((run) => run.completion));
|
|
4154
|
-
for (const result of runResults) {
|
|
4155
|
-
if (result.status === 'rejected') this.appendUnexpectedErrors(errors, result.reason);
|
|
4156
|
-
}
|
|
4157
5360
|
await Promise.allSettled([...state.sessionInitializations.values()]);
|
|
4158
5361
|
errors.push(...await this.closeOrphanedResources(state.storageKey));
|
|
4159
5362
|
await state.scopeQueue;
|
|
@@ -4173,7 +5376,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4173
5376
|
errors.push(...await this.settleDetachedCleanups(state, detachedCleanupAttempts));
|
|
4174
5377
|
for (const stored of state.sessions.values()) {
|
|
4175
5378
|
try {
|
|
4176
|
-
await this.closeStoredSession(stored);
|
|
5379
|
+
await this.closeStoredSession(stored, contextFor(stored.session.sessionId));
|
|
4177
5380
|
} catch (error) {
|
|
4178
5381
|
this.appendUnexpectedErrors(errors, this.projectOperationError(
|
|
4179
5382
|
error,
|
|
@@ -4185,19 +5388,27 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4185
5388
|
}
|
|
4186
5389
|
this.purgeStoredPromptQueue(stored);
|
|
4187
5390
|
}
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
5391
|
+
const roots = this.orderTombstoneRootsChildFirst(
|
|
5392
|
+
state,
|
|
5393
|
+
[...new Set([...state.tombstones.values()].map((tombstone) =>
|
|
5394
|
+
tombstone.rootSessionId ?? tombstone.sessionId))],
|
|
5395
|
+
);
|
|
5396
|
+
for (const rootSessionId of roots) {
|
|
5397
|
+
if (attemptedTombstones.has(rootSessionId)) continue;
|
|
5398
|
+
if (this.descendantTombstoneRoots(state, rootSessionId).length > 0) continue;
|
|
5399
|
+
const retained = state.retainedSessionCleanups.get(rootSessionId);
|
|
4191
5400
|
try {
|
|
4192
5401
|
await this.finishTombstoneCleanup(
|
|
4193
5402
|
state,
|
|
4194
|
-
|
|
4195
|
-
retained?.stored.session.scopeId ?? state.scopeIdHint,
|
|
5403
|
+
rootSessionId,
|
|
5404
|
+
invocation?.scopeId ?? retained?.stored.session.scopeId ?? state.scopeIdHint,
|
|
5405
|
+
invocation,
|
|
4196
5406
|
);
|
|
4197
5407
|
} catch (error) {
|
|
4198
5408
|
this.appendUnexpectedErrors(errors, error);
|
|
4199
5409
|
}
|
|
4200
5410
|
}
|
|
5411
|
+
errors.push(...state.fenceAdditionalErrors.splice(0));
|
|
4201
5412
|
if (errors.length > 0) throw combineErrors(errors);
|
|
4202
5413
|
state.lifecycle = 'retired';
|
|
4203
5414
|
state.sessions.clear();
|
|
@@ -4240,10 +5451,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4240
5451
|
}
|
|
4241
5452
|
this.listeners.clear();
|
|
4242
5453
|
if (errors.length > 0) throw combineErrors(errors);
|
|
5454
|
+
this.compactorInvocationContext.disable();
|
|
4243
5455
|
this.stateLoads.clear();
|
|
4244
5456
|
this.scopeAdmissions.clear();
|
|
4245
5457
|
this.scopeRetirements.clear();
|
|
4246
5458
|
this.storageDrains.clear();
|
|
5459
|
+
this.storageCompactorLifecycleControllers.clear();
|
|
4247
5460
|
}
|
|
4248
5461
|
|
|
4249
5462
|
private appendUnexpectedErrors(target: unknown[], error: unknown): void {
|
|
@@ -4252,6 +5465,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4252
5465
|
} else if (!isAbortError(error)) target.push(error);
|
|
4253
5466
|
}
|
|
4254
5467
|
|
|
5468
|
+
private abortCompactorLifecycle(stored: IStoredSessionState, reason: FlexHarnessAbortError): void {
|
|
5469
|
+
if (!stored.compactorLifecycleController.signal.aborted) {
|
|
5470
|
+
stored.compactorLifecycleController.abort(reason);
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
|
|
4255
5474
|
private async resolveState(
|
|
4256
5475
|
scopeId: string,
|
|
4257
5476
|
): Promise<{ scope: IFlexResolvedScope<TScope>; state: IStorageState }> {
|
|
@@ -4279,10 +5498,26 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4279
5498
|
) throw this.createScopeRetirementError();
|
|
4280
5499
|
stateLoad = this.stateLoads.get(scope.storageKey);
|
|
4281
5500
|
if (!stateLoad) {
|
|
4282
|
-
|
|
5501
|
+
const compactorLifecycleController = new AbortController();
|
|
5502
|
+
this.storageCompactorLifecycleControllers.set(
|
|
5503
|
+
scope.storageKey,
|
|
5504
|
+
compactorLifecycleController,
|
|
5505
|
+
);
|
|
5506
|
+
stateLoad = this.loadState(
|
|
5507
|
+
scope.storageKey,
|
|
5508
|
+
scopeId,
|
|
5509
|
+
scope.scope,
|
|
5510
|
+
compactorLifecycleController,
|
|
5511
|
+
);
|
|
4283
5512
|
this.stateLoads.set(scope.storageKey, stateLoad);
|
|
4284
5513
|
void stateLoad.catch(() => {
|
|
4285
|
-
if (this.stateLoads.get(scope.storageKey) === stateLoad)
|
|
5514
|
+
if (this.stateLoads.get(scope.storageKey) === stateLoad) {
|
|
5515
|
+
this.stateLoads.delete(scope.storageKey);
|
|
5516
|
+
if (
|
|
5517
|
+
this.storageCompactorLifecycleControllers.get(scope.storageKey)
|
|
5518
|
+
=== compactorLifecycleController
|
|
5519
|
+
) this.storageCompactorLifecycleControllers.delete(scope.storageKey);
|
|
5520
|
+
}
|
|
4286
5521
|
});
|
|
4287
5522
|
}
|
|
4288
5523
|
}
|
|
@@ -4328,6 +5563,37 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4328
5563
|
}
|
|
4329
5564
|
}
|
|
4330
5565
|
|
|
5566
|
+
private createCompactorContext(
|
|
5567
|
+
scopeId: string,
|
|
5568
|
+
scope: TScope,
|
|
5569
|
+
storageKey: string,
|
|
5570
|
+
sessionId: string,
|
|
5571
|
+
): IFlexAgentContextInvocation<TScope> {
|
|
5572
|
+
return Object.freeze({ scopeId, scope, storageKey, sessionId });
|
|
5573
|
+
}
|
|
5574
|
+
|
|
5575
|
+
private withCompactorContext<TResult>(
|
|
5576
|
+
context: IFlexAgentContextInvocation<unknown>,
|
|
5577
|
+
operation: () => TResult,
|
|
5578
|
+
): TResult {
|
|
5579
|
+
return this.compactorInvocationContext.run(
|
|
5580
|
+
context as IFlexAgentContextInvocation<TScope>,
|
|
5581
|
+
operation,
|
|
5582
|
+
);
|
|
5583
|
+
}
|
|
5584
|
+
|
|
5585
|
+
private createRunCompactorContext(run: IActiveRun): IFlexAgentContextInvocation<TScope> {
|
|
5586
|
+
return run.originCompactorContext as IFlexAgentContextInvocation<TScope>;
|
|
5587
|
+
}
|
|
5588
|
+
|
|
5589
|
+
private effectiveRunCompactorContext(run: IActiveRun): IFlexAgentContextInvocation<TScope> {
|
|
5590
|
+
return (run.deferredCompactorContext ?? run.originCompactorContext) as IFlexAgentContextInvocation<TScope>;
|
|
5591
|
+
}
|
|
5592
|
+
|
|
5593
|
+
private withRunCompactorContext<TResult>(run: IActiveRun, operation: () => TResult): TResult {
|
|
5594
|
+
return this.withCompactorContext(this.effectiveRunCompactorContext(run), operation);
|
|
5595
|
+
}
|
|
5596
|
+
|
|
4331
5597
|
private requireSession(state: IStorageState, sessionId: string): IStoredSessionState {
|
|
4332
5598
|
validateIdentifier(sessionId, 'sessionId');
|
|
4333
5599
|
requireTransferIdentifier(sessionId, 'sessionId');
|