@modelprofile.com/flexharness 1.0.1 → 2.1.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.
@@ -3,6 +3,7 @@ import {
3
3
  FlexHarnessAbortError,
4
4
  FlexHarnessCallbackOverflowError,
5
5
  FlexHarnessClosedError,
6
+ FlexHarnessExternalError,
6
7
  FlexHarnessNotFoundError,
7
8
  FlexHarnessPermissionRejectedError,
8
9
  FlexHarnessPermissionStateError,
@@ -16,14 +17,18 @@ import type {
16
17
  IFlexAttachmentMessagePart,
17
18
  IFlexCallbackLimits,
18
19
  IFlexCreateSessionOptions,
20
+ IFlexErrorInfo,
19
21
  IFlexHarnessOptions,
20
22
  IFlexHarnessSnapshot,
21
23
  IFlexMessage,
24
+ IFlexMessagePage,
25
+ IFlexMessagePageOptions,
22
26
  IFlexModelIdentity,
23
27
  IFlexPartChangedEvent,
24
28
  IFlexPermissionRequest,
25
29
  IFlexPermissionRequestInput,
26
30
  IFlexPromptOptions,
31
+ IFlexPromptAdmission,
27
32
  IFlexPromptResult,
28
33
  IFlexResolvedModel,
29
34
  IFlexResolvedScope,
@@ -39,6 +44,8 @@ import type {
39
44
  TFlexAgentToolCallFinishEvent,
40
45
  TFlexAgentToolSet,
41
46
  TFlexAttachmentSource,
47
+ TFlexExternalErrorProjector,
48
+ TFlexExternalErrorSource,
42
49
  TFlexHarnessEvent,
43
50
  TFlexHarnessEventListener,
44
51
  TFlexMessagePart,
@@ -75,15 +82,22 @@ interface IStorageState {
75
82
  activeRuns: Map<string, IActiveRun>;
76
83
  pendingPermissions: Map<string, IPendingPermission>;
77
84
  saveTail: Promise<void>;
85
+ lifecycle: 'active' | 'retiring' | 'retired';
86
+ detachedToolCleanupErrors: Error[];
78
87
  }
79
88
 
80
89
  interface IActiveRun {
90
+ state: IStorageState;
81
91
  scopeId: string;
82
92
  sessionId: string;
83
93
  runId: string;
84
94
  userMessageId: string;
85
95
  assistantMessageId: string;
86
96
  controller: AbortController;
97
+ admission: Promise<void>;
98
+ resolveAdmission: () => void;
99
+ rejectAdmission: (error: unknown) => void;
100
+ admissionSettled: boolean;
87
101
  ownerCancellation?: FlexHarnessAbortError;
88
102
  internalFailure?: unknown;
89
103
  finalizer: Promise<IFlexPromptResult>;
@@ -111,12 +125,30 @@ interface IPendingPermission {
111
125
  abortListener: () => void;
112
126
  }
113
127
 
128
+ interface IDetachedToolCleanup {
129
+ state: IStorageState;
130
+ completion: Promise<void>;
131
+ }
132
+
133
+ interface IScopeAdmissionState {
134
+ generation: number;
135
+ inFlightResolvers: number;
136
+ retiring: boolean;
137
+ }
138
+
114
139
  interface IFinalMutationResult {
115
140
  userMessage: IFlexMessage;
116
141
  assistantMessage: IFlexMessage;
117
142
  terminalParts: TFlexMessagePart[];
118
143
  }
119
144
 
145
+ interface IRunResultProjection {
146
+ text: string;
147
+ steps: number;
148
+ finishReason: string;
149
+ usage: IFlexUsage;
150
+ }
151
+
120
152
  type TEventDetails = Record<string, unknown> & {
121
153
  type: TFlexHarnessEvent['type'];
122
154
  };
@@ -132,11 +164,35 @@ interface IResolverOutcome<T> {
132
164
  error?: unknown;
133
165
  }
134
166
 
167
+ interface IFlexMessageCursor {
168
+ version: 1;
169
+ namespace: string;
170
+ sessionId: string;
171
+ anchorMessageId: string;
172
+ }
173
+
135
174
  const DEFAULT_CALLBACK_LIMITS: Required<IFlexCallbackLimits> = {
136
175
  maxEvents: 10_000,
137
176
  maxOutputBytes: 1024 * 1024,
138
177
  maxParts: 2_000,
139
178
  };
179
+ const maxMessagePageSize = 50;
180
+ const maxMessagePageCursorBytes = 4096;
181
+ const maxTransferIdentifierBytes = 512;
182
+ const maxTransferMetadataBytes = 2048;
183
+ const maxTransferTextBytes = 96 * 1024;
184
+ const maxTransferMessageBytes = 480 * 1024;
185
+ const maxTransferPageBytes = 512 * 1024;
186
+ const maxProjectedErrorNameBytes = 128;
187
+ const maxProjectedErrorMessageBytes = 2048;
188
+ const maxProjectedErrorCodeBytes = 128;
189
+ const externalErrorFallback: IFlexErrorInfo = Object.freeze({
190
+ name: 'FlexHarnessExternalError',
191
+ message: 'The model operation failed.',
192
+ code: 'FLEX_EXTERNAL_ERROR',
193
+ });
194
+ const maxDetachedCleanupErrors = 100;
195
+ const scopeRetirementMessage = 'The scope is being retired.';
140
196
 
141
197
  function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
142
198
  return Boolean(
@@ -149,15 +205,21 @@ function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
149
205
  async function* normalizeAsyncIterable(
150
206
  iterable: AsyncIterable<unknown>,
151
207
  limits: Required<NonNullable<IFlexHarnessOptions<unknown>['toolOutputLimits']>>,
208
+ projectError: (error: unknown) => Error,
152
209
  ): AsyncGenerator<unknown> {
153
- for await (const value of iterable) {
154
- yield normalizeJsonValue(value, limits);
210
+ try {
211
+ for await (const value of iterable) {
212
+ yield normalizeJsonValue(value, limits);
213
+ }
214
+ } catch (error) {
215
+ throw projectError(error);
155
216
  }
156
217
  }
157
218
 
158
219
  function wrapToolSet(
159
220
  tools: TFlexAgentToolSet,
160
221
  limits: Required<NonNullable<IFlexHarnessOptions<unknown>['toolOutputLimits']>>,
222
+ projectError: (error: unknown) => Error,
161
223
  ): TFlexAgentToolSet {
162
224
  const wrapped: Record<string, unknown> = {};
163
225
  for (const [name, tool] of Object.entries(tools)) {
@@ -170,11 +232,31 @@ function wrapToolSet(
170
232
  wrapped[name] = {
171
233
  ...toolRecord,
172
234
  execute(input: unknown, options: unknown): unknown {
173
- const output = execute.call(tool, input, options);
174
- if (isAsyncIterable(output)) {
175
- return normalizeAsyncIterable(output, limits);
235
+ let output: unknown;
236
+ try {
237
+ output = execute.call(tool, input, options);
238
+ } catch (error) {
239
+ throw projectError(error);
240
+ }
241
+ try {
242
+ if (isAsyncIterable(output)) {
243
+ return normalizeAsyncIterable(output, limits, projectError);
244
+ }
245
+ } catch (error) {
246
+ throw projectError(error);
176
247
  }
177
- return Promise.resolve(output).then((value) => normalizeJsonValue(value, limits));
248
+ return Promise.resolve(output).then(
249
+ (value) => {
250
+ try {
251
+ return normalizeJsonValue(value, limits);
252
+ } catch (error) {
253
+ throw projectError(error);
254
+ }
255
+ },
256
+ (error: unknown) => {
257
+ throw projectError(error);
258
+ },
259
+ );
178
260
  },
179
261
  };
180
262
  }
@@ -185,25 +267,216 @@ function publicSnapshot<T>(value: T): T {
185
267
  return deepFreeze(cloneSerializable(value));
186
268
  }
187
269
 
270
+ function jsonBytes(value: unknown): number {
271
+ return Buffer.byteLength(JSON.stringify(value), 'utf8');
272
+ }
273
+
274
+ function truncateUtf8(value: string, maxBytes: number): string {
275
+ if (Buffer.byteLength(value, 'utf8') <= maxBytes) return value;
276
+ const suffix = ' [truncated]';
277
+ const suffixBytes = Buffer.byteLength(suffix, 'utf8');
278
+ let truncated = Buffer.from(value, 'utf8')
279
+ .subarray(0, Math.max(0, maxBytes - suffixBytes))
280
+ .toString('utf8')
281
+ .replace(/\uFFFD$/u, '');
282
+ while (Buffer.byteLength(`${truncated}${suffix}`, 'utf8') > maxBytes) {
283
+ truncated = truncated.slice(0, -1);
284
+ }
285
+ return `${truncated}${suffix}`;
286
+ }
287
+
288
+ function requireTransferIdentifier(value: string, field: string): void {
289
+ if (Buffer.byteLength(value, 'utf8') > maxTransferIdentifierBytes) {
290
+ throw new FlexHarnessValidationError(`Stored ${field} exceeds the transfer limit.`);
291
+ }
292
+ }
293
+
294
+ function createBoundedTransferMessage(message: IFlexMessage): IFlexMessage {
295
+ const projected = cloneSerializable(message);
296
+ requireTransferIdentifier(projected.messageId, 'messageId');
297
+ requireTransferIdentifier(projected.sessionId, 'sessionId');
298
+ requireTransferIdentifier(projected.runId, 'runId');
299
+ projected.createdAt = truncateUtf8(projected.createdAt, 128);
300
+ if (projected.completedAt !== undefined) {
301
+ projected.completedAt = truncateUtf8(projected.completedAt, 128);
302
+ }
303
+ if (projected.error !== undefined) {
304
+ projected.error = truncateUtf8(projected.error, maxTransferMetadataBytes);
305
+ }
306
+ if (projected.model) {
307
+ projected.model.provider = truncateUtf8(projected.model.provider, maxTransferIdentifierBytes);
308
+ projected.model.model = truncateUtf8(projected.model.model, maxTransferIdentifierBytes);
309
+ if (projected.model.displayName !== undefined) {
310
+ projected.model.displayName = truncateUtf8(
311
+ projected.model.displayName,
312
+ maxTransferMetadataBytes,
313
+ );
314
+ }
315
+ if (projected.model.variant !== undefined) {
316
+ projected.model.variant = truncateUtf8(projected.model.variant, 128);
317
+ }
318
+ }
319
+ for (const part of projected.parts) {
320
+ requireTransferIdentifier(part.partId, 'partId');
321
+ if (part.type === 'text' || part.type === 'reasoning') {
322
+ part.text = truncateUtf8(part.text, maxTransferTextBytes);
323
+ } else if (part.type === 'tool') {
324
+ part.toolCallId = truncateUtf8(part.toolCallId, maxTransferIdentifierBytes);
325
+ part.toolName = truncateUtf8(part.toolName, maxTransferIdentifierBytes);
326
+ if (part.error !== undefined) {
327
+ part.error = truncateUtf8(part.error, maxTransferMetadataBytes);
328
+ }
329
+ } else {
330
+ if (part.mediaType !== undefined) {
331
+ part.mediaType = truncateUtf8(part.mediaType, maxTransferIdentifierBytes);
332
+ }
333
+ if (part.name !== undefined) {
334
+ part.name = truncateUtf8(part.name, maxTransferMetadataBytes);
335
+ }
336
+ }
337
+ }
338
+ if (jsonBytes(projected) > maxTransferMessageBytes) {
339
+ projected.parts = [{
340
+ partId: 'transfer-elided',
341
+ type: 'text',
342
+ text: '[elided: message exceeds the transfer budget]',
343
+ }];
344
+ }
345
+ if (jsonBytes(projected) > maxTransferMessageBytes) {
346
+ throw new FlexHarnessValidationError('Stored message metadata exceeds the transfer limit.');
347
+ }
348
+ return projected;
349
+ }
350
+
351
+ function validateProjectedErrorInfo(value: unknown): IFlexErrorInfo {
352
+ if (
353
+ !value
354
+ || typeof value !== 'object'
355
+ || Array.isArray(value)
356
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(value))
357
+ ) {
358
+ throw new FlexHarnessValidationError('External error projection is invalid.');
359
+ }
360
+ const descriptors = Object.getOwnPropertyDescriptors(value);
361
+ const keys = Reflect.ownKeys(descriptors);
362
+ const unsupportedKey = keys.find(
363
+ (key) => typeof key !== 'string' || (key !== 'name' && key !== 'message' && key !== 'code'),
364
+ );
365
+ const name = descriptors.name?.value;
366
+ const message = descriptors.message?.value;
367
+ const code = descriptors.code?.value;
368
+ if (
369
+ unsupportedKey
370
+ || descriptors.name?.get !== undefined
371
+ || descriptors.name?.set !== undefined
372
+ || descriptors.message?.get !== undefined
373
+ || descriptors.message?.set !== undefined
374
+ || descriptors.code?.get !== undefined
375
+ || descriptors.code?.set !== undefined
376
+ || typeof name !== 'string'
377
+ || name.length === 0
378
+ || Buffer.byteLength(name, 'utf8') > maxProjectedErrorNameBytes
379
+ || typeof message !== 'string'
380
+ || message.length === 0
381
+ || Buffer.byteLength(message, 'utf8') > maxProjectedErrorMessageBytes
382
+ || (code !== undefined
383
+ && (
384
+ typeof code !== 'string'
385
+ || code.length === 0
386
+ || Buffer.byteLength(code, 'utf8') > maxProjectedErrorCodeBytes
387
+ ))
388
+ ) {
389
+ throw new FlexHarnessValidationError('External error projection is invalid.');
390
+ }
391
+ return Object.freeze({ name, message, ...(code === undefined ? {} : { code }) });
392
+ }
393
+
188
394
  function validateIdentifier(value: string, name: string): void {
189
395
  if (typeof value !== 'string' || value.length === 0) {
190
396
  throw new FlexHarnessValidationError(`${name} must be a non-empty string.`);
191
397
  }
192
398
  }
193
399
 
194
- function validateModelIdentity(identity: IFlexModelIdentity): void {
195
- validateIdentifier(identity.provider, 'model identity provider');
196
- validateIdentifier(identity.model, 'model identity model');
197
- if (identity.displayName !== undefined) {
198
- validateIdentifier(identity.displayName, 'model identity displayName');
400
+ function normalizeModelIdentity(identity: IFlexModelIdentity): IFlexModelIdentity {
401
+ const provider = identity.provider;
402
+ const model = identity.model;
403
+ const displayName = identity.displayName;
404
+ const variant = identity.variant;
405
+ const normalized: IFlexModelIdentity = {
406
+ provider,
407
+ model,
408
+ ...(displayName === undefined ? {} : { displayName }),
409
+ ...(variant === undefined ? {} : { variant }),
410
+ };
411
+ validateIdentifier(normalized.provider, 'model identity provider');
412
+ validateIdentifier(normalized.model, 'model identity model');
413
+ if (normalized.displayName !== undefined) {
414
+ validateIdentifier(normalized.displayName, 'model identity displayName');
415
+ }
416
+ if (normalized.variant !== undefined) {
417
+ validateIdentifier(normalized.variant, 'model identity variant');
418
+ }
419
+ if (
420
+ Buffer.byteLength(normalized.provider, 'utf8') > maxTransferIdentifierBytes
421
+ || Buffer.byteLength(normalized.model, 'utf8') > maxTransferIdentifierBytes
422
+ || (normalized.displayName !== undefined
423
+ && Buffer.byteLength(normalized.displayName, 'utf8') > maxTransferMetadataBytes)
424
+ || (normalized.variant !== undefined && Buffer.byteLength(normalized.variant, 'utf8') > 128)
425
+ ) {
426
+ throw new FlexHarnessValidationError('Model identity exceeds its transfer limit.');
427
+ }
428
+ return normalized;
429
+ }
430
+
431
+ function normalizeResolvedModel(resolvedModel: IFlexResolvedModel): IFlexResolvedModel {
432
+ const model = resolvedModel.model;
433
+ const identity = normalizeModelIdentity(resolvedModel.identity);
434
+ const system = resolvedModel.system;
435
+ const providerOptions = resolvedModel.providerOptions;
436
+ const cache = resolvedModel.cache;
437
+ const maxSteps = resolvedModel.maxSteps;
438
+ return {
439
+ model,
440
+ identity,
441
+ ...(system === undefined ? {} : { system }),
442
+ ...(providerOptions === undefined ? {} : { providerOptions }),
443
+ ...(cache === undefined ? {} : { cache }),
444
+ ...(maxSteps === undefined ? {} : { maxSteps }),
445
+ };
446
+ }
447
+
448
+ function normalizeRunResult(result: TFlexAgentRunResult): IRunResultProjection {
449
+ const normalized: IRunResultProjection = {
450
+ text: result.text,
451
+ steps: result.steps,
452
+ finishReason: result.finishReason,
453
+ usage: {
454
+ inputTokens: result.usage.inputTokens,
455
+ outputTokens: result.usage.outputTokens,
456
+ totalTokens: result.usage.totalTokens,
457
+ cacheReadTokens: result.usage.cacheReadTokens,
458
+ cacheWriteTokens: result.usage.cacheWriteTokens,
459
+ },
460
+ };
461
+ if (
462
+ typeof normalized.text !== 'string'
463
+ || typeof normalized.finishReason !== 'string'
464
+ || !Number.isSafeInteger(normalized.steps)
465
+ || normalized.steps < 0
466
+ || Object.values(normalized.usage).some((value) => !Number.isFinite(value) || value < 0)
467
+ ) {
468
+ throw new FlexHarnessValidationError('Agent runner result is invalid.');
199
469
  }
470
+ return normalized;
200
471
  }
201
472
 
202
473
  function combineErrors(errors: unknown[]): unknown {
203
474
  if (errors.length === 1) {
204
475
  return errors[0];
205
476
  }
206
- return new FlexHarnessRunError(errors);
477
+ const combined = new FlexHarnessRunError(errors);
478
+ Object.freeze(combined.errors);
479
+ return Object.freeze(combined);
207
480
  }
208
481
 
209
482
  function isAbortError(error: unknown): boolean {
@@ -214,7 +487,7 @@ function isAbortError(error: unknown): boolean {
214
487
  );
215
488
  }
216
489
 
217
- function makeUsage(result: TFlexAgentRunResult): IFlexUsage {
490
+ function makeUsage(result: IRunResultProjection): IFlexUsage {
218
491
  return {
219
492
  inputTokens: result.usage.inputTokens,
220
493
  outputTokens: result.usage.outputTokens,
@@ -329,9 +602,14 @@ export class FlexHarness<TScope = unknown> {
329
602
  NonNullable<IFlexHarnessOptions<TScope>['toolOutputLimits']>
330
603
  >;
331
604
  private readonly callbackLimits: Required<IFlexCallbackLimits>;
605
+ private readonly externalErrorProjector?: TFlexExternalErrorProjector;
332
606
  private readonly stateLoads = new Map<string, Promise<IStorageState>>();
607
+ private readonly scopeAdmissions = new Map<string, IScopeAdmissionState>();
608
+ private readonly scopeRetirements = new Map<string, Promise<void>>();
609
+ private readonly storageDrains = new Map<string, Promise<void>>();
333
610
  private readonly listeners = new Set<TFlexHarnessEventListener>();
334
- private readonly detachedToolCleanups = new Set<Promise<void>>();
611
+ private readonly detachedToolCleanups = new Set<IDetachedToolCleanup>();
612
+ private readonly trustedInternalErrors = new WeakSet<object>();
335
613
  private sequence = 0;
336
614
  private closed = false;
337
615
  private disposePromise?: Promise<void>;
@@ -344,11 +622,12 @@ export class FlexHarness<TScope = unknown> {
344
622
  this.runner = options.runner ?? plugins.runAgent;
345
623
  this.toolOutputLimits = resolveJsonLimits(options.toolOutputLimits);
346
624
  this.callbackLimits = resolveCallbackLimits(options.callbackLimits);
625
+ this.externalErrorProjector = options.externalErrorProjector;
347
626
  }
348
627
 
349
628
  public async listSessions(scopeId: string): Promise<IFlexSession[]> {
350
629
  const { state } = await this.resolveState(scopeId);
351
- await state.saveTail;
630
+ await this.waitForReadableState(state);
352
631
  return publicSnapshot(
353
632
  [...state.sessions.values()]
354
633
  .map((entry) => entry.session)
@@ -397,7 +676,7 @@ export class FlexHarness<TScope = unknown> {
397
676
 
398
677
  public async getSession(scopeId: string, sessionId: string): Promise<IFlexSession> {
399
678
  const { state } = await this.resolveState(scopeId);
400
- await state.saveTail;
679
+ await this.waitForReadableState(state);
401
680
  return publicSnapshot(this.requireSession(state, sessionId).session);
402
681
  }
403
682
 
@@ -449,16 +728,126 @@ export class FlexHarness<TScope = unknown> {
449
728
 
450
729
  public async getMessages(scopeId: string, sessionId: string): Promise<IFlexMessage[]> {
451
730
  const { state } = await this.resolveState(scopeId);
452
- await state.saveTail;
731
+ await this.waitForReadableState(state);
453
732
  return publicSnapshot(this.requireSession(state, sessionId).messages);
454
733
  }
455
734
 
735
+ public async listMessagePage(
736
+ scopeId: string,
737
+ sessionId: string,
738
+ options: IFlexMessagePageOptions = {},
739
+ ): Promise<IFlexMessagePage> {
740
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
741
+ throw new FlexHarnessValidationError('Message page options must be a plain object.');
742
+ }
743
+ const unsupportedKey = Object.keys(options).find(
744
+ (key) => key !== 'limit' && key !== 'before',
745
+ );
746
+ if (unsupportedKey) {
747
+ throw new FlexHarnessValidationError(
748
+ `Message page options do not support "${unsupportedKey}".`,
749
+ );
750
+ }
751
+ const limit = options.limit ?? maxMessagePageSize;
752
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > maxMessagePageSize) {
753
+ throw new FlexHarnessValidationError(
754
+ `Message page limit must be an integer from 1 through ${maxMessagePageSize}.`,
755
+ );
756
+ }
757
+ if (
758
+ options.before !== undefined
759
+ && (
760
+ typeof options.before !== 'string'
761
+ || options.before.length === 0
762
+ || Buffer.byteLength(options.before, 'utf8') > maxMessagePageCursorBytes
763
+ )
764
+ ) {
765
+ throw new FlexHarnessValidationError('Message page cursor is invalid.');
766
+ }
767
+ validateIdentifier(sessionId, 'sessionId');
768
+ requireTransferIdentifier(sessionId, 'sessionId');
769
+ const { state } = await this.resolveState(scopeId);
770
+ await this.waitForReadableState(state);
771
+ const stored = this.requireSession(state, sessionId);
772
+ const namespace = this.messageCursorNamespace(state.storageKey);
773
+ let end = stored.messages.length;
774
+ if (options.before !== undefined) {
775
+ const cursor = this.parseMessageCursor(options.before);
776
+ if (cursor.namespace !== namespace || cursor.sessionId !== sessionId) {
777
+ throw new FlexHarnessValidationError('Message page cursor is invalid.');
778
+ }
779
+ const anchorIndex = stored.messages.findIndex(
780
+ (message) => message.messageId === cursor.anchorMessageId,
781
+ );
782
+ if (anchorIndex < 0) {
783
+ throw new FlexHarnessValidationError('Message page cursor is stale.');
784
+ }
785
+ end = anchorIndex;
786
+ }
787
+
788
+ let start = end;
789
+ let messages: IFlexMessage[] = [];
790
+ for (let index = end - 1; index >= 0 && messages.length < limit; index--) {
791
+ const candidate = createBoundedTransferMessage(stored.messages[index]);
792
+ const candidateMessages = [candidate, ...messages];
793
+ const nextCursor = index > 0
794
+ ? this.createMessageCursor(namespace, sessionId, candidate.messageId)
795
+ : undefined;
796
+ const candidatePage: IFlexMessagePage = {
797
+ messages: candidateMessages,
798
+ ...(nextCursor === undefined ? {} : { nextCursor }),
799
+ };
800
+ if (jsonBytes(candidatePage) > maxTransferPageBytes) break;
801
+ start = index;
802
+ messages = candidateMessages;
803
+ }
804
+ if (messages.length === 0 && end > 0) {
805
+ throw new FlexHarnessValidationError('Stored message exceeds the page transfer limit.');
806
+ }
807
+ const nextCursor = start > 0
808
+ ? this.createMessageCursor(namespace, sessionId, messages[0].messageId)
809
+ : undefined;
810
+ const page: IFlexMessagePage = {
811
+ messages,
812
+ ...(nextCursor === undefined ? {} : { nextCursor }),
813
+ };
814
+ if (jsonBytes(page) > maxTransferPageBytes) {
815
+ throw new FlexHarnessValidationError('Message page exceeds the transfer limit.');
816
+ }
817
+ return publicSnapshot(page);
818
+ }
819
+
820
+ public async getMessage(
821
+ scopeId: string,
822
+ sessionId: string,
823
+ messageId: string,
824
+ ): Promise<IFlexMessage> {
825
+ validateIdentifier(sessionId, 'sessionId');
826
+ requireTransferIdentifier(sessionId, 'sessionId');
827
+ validateIdentifier(messageId, 'messageId');
828
+ requireTransferIdentifier(messageId, 'messageId');
829
+ const { state } = await this.resolveState(scopeId);
830
+ await this.waitForReadableState(state);
831
+ const message = this.requireMessage(this.requireSession(state, sessionId), messageId);
832
+ return publicSnapshot(createBoundedTransferMessage(message));
833
+ }
834
+
456
835
  public async prompt(
457
836
  scopeId: string,
458
837
  sessionId: string,
459
838
  prompt: TFlexPrompt,
460
839
  options: IFlexPromptOptions = {},
461
840
  ): Promise<IFlexPromptResult> {
841
+ const admission = await this.startPrompt(scopeId, sessionId, prompt, options);
842
+ return admission.completion;
843
+ }
844
+
845
+ public async startPrompt(
846
+ scopeId: string,
847
+ sessionId: string,
848
+ prompt: TFlexPrompt,
849
+ options: IFlexPromptOptions = {},
850
+ ): Promise<IFlexPromptAdmission> {
462
851
  const normalizedPrompt = normalizeFlexPrompt(prompt);
463
852
  if (options.maxSteps !== undefined && (!Number.isSafeInteger(options.maxSteps) || options.maxSteps < 1)) {
464
853
  throw new FlexHarnessValidationError('maxSteps must be a positive integer.');
@@ -466,19 +855,38 @@ export class FlexHarness<TScope = unknown> {
466
855
  const resolved = await this.resolveState(scopeId);
467
856
  await resolved.state.saveTail;
468
857
  this.assertOpen();
858
+ this.assertStateAcceptingWork(resolved.state);
469
859
  this.requireSession(resolved.state, sessionId);
470
860
  if (resolved.state.activeRuns.has(sessionId)) {
471
861
  throw new FlexHarnessSessionBusyError(sessionId);
472
862
  }
473
863
 
864
+ let resolveAdmission!: () => void;
865
+ let rejectAdmission!: (error: unknown) => void;
866
+ const admission = new Promise<void>((resolve, reject) => {
867
+ resolveAdmission = resolve;
868
+ rejectAdmission = reject;
869
+ });
870
+ void admission.catch(() => undefined);
871
+ let resolveCompletion!: (result: IFlexPromptResult) => void;
872
+ let rejectCompletion!: (error: unknown) => void;
873
+ const completion = new Promise<IFlexPromptResult>((resolve, reject) => {
874
+ resolveCompletion = resolve;
875
+ rejectCompletion = reject;
876
+ });
474
877
  const run: IActiveRun = {
878
+ state: resolved.state,
475
879
  scopeId,
476
880
  sessionId,
477
881
  runId: plugins.crypto.randomUUID(),
478
882
  userMessageId: plugins.crypto.randomUUID(),
479
883
  assistantMessageId: plugins.crypto.randomUUID(),
480
884
  controller: new AbortController(),
481
- finalizer: Promise.reject(new Error('Run finalizer was not initialized.')),
885
+ admission,
886
+ resolveAdmission,
887
+ rejectAdmission,
888
+ admissionSettled: false,
889
+ finalizer: completion,
482
890
  callbackEventCount: 0,
483
891
  callbackOutputBytes: 0,
484
892
  callbackParts: [],
@@ -491,17 +899,21 @@ export class FlexHarness<TScope = unknown> {
491
899
  };
492
900
  void run.finalizer.catch(() => undefined);
493
901
  resolved.state.activeRuns.set(sessionId, run);
494
- run.finalizer = this.executeRun(resolved, run, normalizedPrompt, options);
495
- return run.finalizer;
902
+ void this.executeRun(resolved, run, normalizedPrompt, options).then(
903
+ resolveCompletion,
904
+ rejectCompletion,
905
+ );
906
+ await run.admission;
907
+ return Object.freeze({ runId: run.runId, completion: run.finalizer });
496
908
  }
497
909
 
498
- public async abort(scopeId: string, sessionId: string, reason?: string): Promise<boolean> {
910
+ public async abort(scopeId: string, sessionId: string, _reason?: string): Promise<boolean> {
499
911
  const { state } = await this.resolveState(scopeId);
500
912
  const run = state.activeRuns.get(sessionId);
501
913
  if (!run || run.phase === 'committing') {
502
914
  return false;
503
915
  }
504
- const error = new FlexHarnessAbortError(reason ?? 'The run was aborted by the caller.');
916
+ const error = Object.freeze(new FlexHarnessAbortError());
505
917
  if (run.internalFailure === undefined) run.ownerCancellation ??= error;
506
918
  this.rejectRunPermissions(state, run, error);
507
919
  run.controller.abort(error);
@@ -513,7 +925,7 @@ export class FlexHarness<TScope = unknown> {
513
925
  sessionId?: string,
514
926
  ): Promise<IFlexPermissionRequest[]> {
515
927
  const { state } = await this.resolveState(scopeId);
516
- await state.saveTail;
928
+ await this.waitForReadableState(state);
517
929
  return publicSnapshot(
518
930
  [...state.pendingPermissions.values()]
519
931
  .map((pending) => pending.request)
@@ -554,6 +966,27 @@ export class FlexHarness<TScope = unknown> {
554
966
  };
555
967
  }
556
968
 
969
+ public retireScope(scopeId: string): Promise<void> {
970
+ this.assertOpen();
971
+ validateIdentifier(scopeId, 'scopeId');
972
+ const existingRetirement = this.scopeRetirements.get(scopeId);
973
+ if (existingRetirement) return existingRetirement;
974
+
975
+ const admission = this.getScopeAdmission(scopeId);
976
+ admission.generation++;
977
+ admission.retiring = true;
978
+ let retirement!: Promise<void>;
979
+ retirement = this.retireScopeInternal(scopeId).finally(() => {
980
+ if (this.scopeRetirements.get(scopeId) === retirement) {
981
+ this.scopeRetirements.delete(scopeId);
982
+ }
983
+ admission.retiring = false;
984
+ this.pruneScopeAdmission(scopeId, admission);
985
+ });
986
+ this.scopeRetirements.set(scopeId, retirement);
987
+ return retirement;
988
+ }
989
+
557
990
  public async dispose(): Promise<void> {
558
991
  if (this.disposePromise) {
559
992
  return this.disposePromise;
@@ -564,52 +997,31 @@ export class FlexHarness<TScope = unknown> {
564
997
  }
565
998
 
566
999
  private async disposeInternal(): Promise<void> {
567
- const states = (
568
- await Promise.allSettled([...this.stateLoads.values()])
569
- ).flatMap((result) => (result.status === 'fulfilled' ? [result.value] : []));
570
- const finalizers: Promise<IFlexPromptResult>[] = [];
571
- for (const state of states) {
572
- for (const run of state.activeRuns.values()) {
573
- finalizers.push(run.finalizer);
574
- if (run.phase === 'committing') continue;
575
- const error = new FlexHarnessAbortError('The run was aborted because FlexHarness was disposed.');
576
- if (run.internalFailure === undefined) run.ownerCancellation ??= error;
577
- this.rejectRunPermissions(state, run, error);
578
- run.controller.abort(error);
579
- }
580
- for (const pending of [...state.pendingPermissions.values()]) {
581
- this.rejectPending(
582
- state,
583
- pending,
584
- new FlexHarnessAbortError('Permission was rejected because FlexHarness was disposed.'),
585
- );
586
- }
587
- }
588
- const results = await Promise.allSettled(finalizers);
589
- await Promise.all(states.map((state) => state.saveTail));
590
- const detachedCleanupResults = await Promise.allSettled([...this.detachedToolCleanups]);
1000
+ const stateLoads = [...this.stateLoads.entries()];
1001
+ const results = await Promise.allSettled(
1002
+ stateLoads.map(([storageKey, stateLoad]) =>
1003
+ this.drainStorage(
1004
+ storageKey,
1005
+ stateLoad,
1006
+ this.trustInternalError(
1007
+ new FlexHarnessAbortError('The run was aborted because FlexHarness was disposed.'),
1008
+ ),
1009
+ ),
1010
+ ),
1011
+ );
1012
+ await Promise.all([...this.detachedToolCleanups].map((cleanup) => cleanup.completion));
591
1013
  this.listeners.clear();
592
1014
  this.stateLoads.clear();
1015
+ this.scopeAdmissions.clear();
1016
+ this.scopeRetirements.clear();
1017
+ this.storageDrains.clear();
593
1018
  this.detachedToolCleanups.clear();
594
- for (const state of states) {
595
- state.sessions.clear();
596
- state.activeRuns.clear();
597
- state.pendingPermissions.clear();
598
- }
599
1019
  const unexpectedErrors: unknown[] = [];
600
1020
  for (const result of results) {
601
- if (result.status === 'fulfilled') continue;
602
- if (result.reason instanceof FlexHarnessRunError) {
603
- for (const error of result.reason.errors) {
604
- if (!isAbortError(error)) unexpectedErrors.push(error);
605
- }
606
- } else if (!isAbortError(result.reason)) {
607
- unexpectedErrors.push(result.reason);
1021
+ if (result.status === 'rejected') {
1022
+ this.appendUnexpectedErrors(unexpectedErrors, result.reason);
608
1023
  }
609
1024
  }
610
- for (const result of detachedCleanupResults) {
611
- if (result.status === 'rejected') unexpectedErrors.push(result.reason);
612
- }
613
1025
  if (unexpectedErrors.length > 0) {
614
1026
  throw combineErrors(unexpectedErrors);
615
1027
  }
@@ -623,7 +1035,7 @@ export class FlexHarness<TScope = unknown> {
623
1035
  ): Promise<IFlexPromptResult> {
624
1036
  let modelResolution: IFlexResolvedModel | undefined;
625
1037
  let toolHandle: IFlexToolHandle | undefined;
626
- let result: TFlexAgentRunResult | undefined;
1038
+ let result: IRunResultProjection | undefined;
627
1039
  let serializedResultMessages: TFlexAgentModelMessage[] | undefined;
628
1040
  let originalError: unknown;
629
1041
  let reserved = false;
@@ -649,6 +1061,7 @@ export class FlexHarness<TScope = unknown> {
649
1061
  messageId: run.assistantMessageId,
650
1062
  message: reservation.assistantMessage,
651
1063
  });
1064
+ this.resolveRunAdmission(run);
652
1065
 
653
1066
  const modelOutcome = Promise.resolve()
654
1067
  .then(() => this.modelResolver.resolveModel({
@@ -660,8 +1073,16 @@ export class FlexHarness<TScope = unknown> {
660
1073
  signal: run.controller.signal,
661
1074
  }))
662
1075
  .then(
663
- (value): IResolverOutcome<IFlexResolvedModel> => ({ source: 'model', success: true, value }),
664
- (error: unknown): IResolverOutcome<IFlexResolvedModel> => ({ source: 'model', success: false, error }),
1076
+ (value): IResolverOutcome<IFlexResolvedModel> => ({
1077
+ source: 'model',
1078
+ success: true,
1079
+ value,
1080
+ }),
1081
+ (error: unknown): IResolverOutcome<IFlexResolvedModel> => ({
1082
+ source: 'model',
1083
+ success: false,
1084
+ error,
1085
+ }),
665
1086
  );
666
1087
  const toolOutcome = Promise.resolve()
667
1088
  .then(() =>
@@ -678,8 +1099,16 @@ export class FlexHarness<TScope = unknown> {
678
1099
  : undefined,
679
1100
  )
680
1101
  .then(
681
- (value): IResolverOutcome<IFlexToolHandle | undefined> => ({ source: 'tools', success: true, value }),
682
- (error: unknown): IResolverOutcome<IFlexToolHandle | undefined> => ({ source: 'tools', success: false, error }),
1102
+ (value): IResolverOutcome<IFlexToolHandle | undefined> => ({
1103
+ source: 'tools',
1104
+ success: true,
1105
+ value,
1106
+ }),
1107
+ (error: unknown): IResolverOutcome<IFlexToolHandle | undefined> => ({
1108
+ source: 'tools',
1109
+ success: false,
1110
+ error,
1111
+ }),
683
1112
  );
684
1113
  let resolveAbort!: (outcome: IResolverOutcome<never>) => void;
685
1114
  const abortOutcome = new Promise<IResolverOutcome<never>>((resolve) => {
@@ -698,27 +1127,41 @@ export class FlexHarness<TScope = unknown> {
698
1127
  const firstResolver = await Promise.race([modelOutcome, toolOutcome, abortOutcome]);
699
1128
  if (!firstResolver.success) {
700
1129
  run.controller.signal.removeEventListener('abort', onResolverAbort);
1130
+ const firstError = firstResolver.source === 'abort'
1131
+ ? firstResolver.error
1132
+ : this.projectExternalError(
1133
+ run,
1134
+ firstResolver.error,
1135
+ firstResolver.source === 'model' ? 'modelResolver' : 'toolProvider',
1136
+ );
701
1137
  if (firstResolver.source !== 'abort') {
702
- this.abortRunInternally(run, firstResolver.error);
1138
+ this.abortRunInternally(run, firstError);
703
1139
  }
704
1140
  if (firstResolver.source !== 'tools') {
705
- this.observeDetachedToolProvider(toolOutcome);
1141
+ this.observeDetachedToolProvider(toolOutcome, run);
706
1142
  }
707
- throw firstResolver.error;
1143
+ throw firstError;
708
1144
  }
709
1145
 
710
- if (firstResolver.source === 'model') {
711
- modelResolution = firstResolver.value as IFlexResolvedModel;
712
- try {
713
- validateModelIdentity(modelResolution.identity);
714
- } catch (error) {
715
- run.controller.signal.removeEventListener('abort', onResolverAbort);
716
- this.abortRunInternally(run, error);
717
- this.observeDetachedToolProvider(toolOutcome);
718
- throw error;
1146
+ try {
1147
+ if (firstResolver.source === 'model') {
1148
+ modelResolution = normalizeResolvedModel(firstResolver.value as IFlexResolvedModel);
1149
+ } else {
1150
+ toolHandle = this.normalizeToolHandle(
1151
+ run,
1152
+ firstResolver.value as IFlexToolHandle | undefined,
1153
+ );
719
1154
  }
720
- } else {
721
- toolHandle = firstResolver.value as IFlexToolHandle | undefined;
1155
+ } catch (error) {
1156
+ const resolverError = firstResolver.source === 'model'
1157
+ ? this.projectExternalError(run, error, 'modelResolver')
1158
+ : error;
1159
+ run.controller.signal.removeEventListener('abort', onResolverAbort);
1160
+ this.abortRunInternally(run, resolverError);
1161
+ if (firstResolver.source === 'model') {
1162
+ this.observeDetachedToolProvider(toolOutcome, run);
1163
+ }
1164
+ throw resolverError;
722
1165
  }
723
1166
 
724
1167
  const secondResolver = await Promise.race([
@@ -727,29 +1170,53 @@ export class FlexHarness<TScope = unknown> {
727
1170
  ]);
728
1171
  run.controller.signal.removeEventListener('abort', onResolverAbort);
729
1172
  if (!secondResolver.success) {
1173
+ const secondError = secondResolver.source === 'abort'
1174
+ ? secondResolver.error
1175
+ : this.projectExternalError(
1176
+ run,
1177
+ secondResolver.error,
1178
+ secondResolver.source === 'model' ? 'modelResolver' : 'toolProvider',
1179
+ );
730
1180
  if (secondResolver.source !== 'abort') {
731
- this.abortRunInternally(run, secondResolver.error);
1181
+ this.abortRunInternally(run, secondError);
732
1182
  }
733
1183
  if (firstResolver.source === 'model' && secondResolver.source === 'abort') {
734
- this.observeDetachedToolProvider(toolOutcome);
1184
+ this.observeDetachedToolProvider(toolOutcome, run);
735
1185
  }
736
- throw secondResolver.error;
1186
+ throw secondError;
737
1187
  }
738
- if (secondResolver.source === 'model') {
739
- modelResolution = secondResolver.value as IFlexResolvedModel;
740
- try {
741
- validateModelIdentity(modelResolution.identity);
742
- } catch (error) {
743
- this.abortRunInternally(run, error);
744
- throw error;
1188
+ try {
1189
+ if (secondResolver.source === 'model') {
1190
+ modelResolution = normalizeResolvedModel(secondResolver.value as IFlexResolvedModel);
1191
+ } else {
1192
+ toolHandle = this.normalizeToolHandle(
1193
+ run,
1194
+ secondResolver.value as IFlexToolHandle | undefined,
1195
+ );
745
1196
  }
746
- } else {
747
- toolHandle = secondResolver.value as IFlexToolHandle | undefined;
1197
+ } catch (error) {
1198
+ const resolverError = secondResolver.source === 'model'
1199
+ ? this.projectExternalError(run, error, 'modelResolver')
1200
+ : error;
1201
+ this.abortRunInternally(run, resolverError);
1202
+ throw resolverError;
748
1203
  }
749
1204
  if (run.controller.signal.aborted) {
750
1205
  throw run.controller.signal.reason;
751
1206
  }
752
1207
 
1208
+ let tools: TFlexAgentToolSet | undefined;
1209
+ try {
1210
+ tools = toolHandle
1211
+ ? wrapToolSet(
1212
+ toolHandle.tools,
1213
+ this.toolOutputLimits,
1214
+ (error) => this.projectExternalError(run, error, 'toolExecution'),
1215
+ )
1216
+ : undefined;
1217
+ } catch (error) {
1218
+ throw this.projectExternalError(run, error, 'toolProvider');
1219
+ }
753
1220
  const runnerOptions: Parameters<typeof this.runner>[0] = {
754
1221
  model: modelResolution!.model,
755
1222
  prompt: prompt.agentPrompt,
@@ -759,7 +1226,7 @@ export class FlexHarness<TScope = unknown> {
759
1226
  ...(options.system ?? modelResolution!.system
760
1227
  ? { system: options.system ?? modelResolution!.system }
761
1228
  : {}),
762
- ...(toolHandle ? { tools: wrapToolSet(toolHandle.tools, this.toolOutputLimits) } : {}),
1229
+ ...(tools ? { tools } : {}),
763
1230
  ...(modelResolution!.providerOptions
764
1231
  ? { providerOptions: modelResolution!.providerOptions }
765
1232
  : {}),
@@ -775,16 +1242,22 @@ export class FlexHarness<TScope = unknown> {
775
1242
  onToolCallFinish: (event) => this.onToolFinish(run, event),
776
1243
  };
777
1244
  run.phase = 'running';
778
- result = await this.runner(runnerOptions);
1245
+ try {
1246
+ const runnerResult = await this.runner(runnerOptions);
1247
+ serializedResultMessages = serializeAgentMessages(runnerResult.messages);
1248
+ result = normalizeRunResult(runnerResult);
1249
+ } catch (error) {
1250
+ throw this.projectExternalError(run, error, 'runner');
1251
+ }
779
1252
  if (run.callbackError) {
780
1253
  throw run.callbackError;
781
1254
  }
782
1255
  if (run.controller.signal.aborted) {
783
1256
  throw run.controller.signal.reason;
784
1257
  }
785
- serializedResultMessages = serializeAgentMessages(result.messages);
786
1258
  } catch (error) {
787
- originalError = error ?? new Error('The run failed without an error value.');
1259
+ const runError = error ?? new Error('The run failed without an error value.');
1260
+ originalError = reserved ? this.projectExternalError(run, runError, 'runner') : runError;
788
1261
  }
789
1262
  run.callbacksClosed = true;
790
1263
  run.phase = 'closing';
@@ -793,15 +1266,18 @@ export class FlexHarness<TScope = unknown> {
793
1266
  ? run.callbackError
794
1267
  : combineErrors([originalError, run.callbackError]);
795
1268
  }
1269
+ if (!reserved) {
1270
+ const admissionError = this.projectExternalError(run, originalError, 'persistence');
1271
+ this.rejectRunAdmission(run, admissionError);
1272
+ if (resolved.state.activeRuns.get(run.sessionId) === run) {
1273
+ resolved.state.activeRuns.delete(run.sessionId);
1274
+ }
1275
+ throw admissionError;
1276
+ }
796
1277
  if (originalError !== undefined && run.ownerCancellation === undefined) {
797
1278
  run.internalFailure ??= originalError;
798
1279
  }
799
1280
 
800
- if (!reserved) {
801
- resolved.state.activeRuns.delete(run.sessionId);
802
- throw originalError;
803
- }
804
-
805
1281
  this.rejectRunPermissions(
806
1282
  resolved.state,
807
1283
  run,
@@ -810,7 +1286,11 @@ export class FlexHarness<TScope = unknown> {
810
1286
  new FlexHarnessAbortError('The run ended before permission was resolved.'),
811
1287
  );
812
1288
  const closeResult = await Promise.allSettled([
813
- Promise.resolve().then(() => toolHandle?.close?.()),
1289
+ Promise.resolve()
1290
+ .then(() => toolHandle?.close?.())
1291
+ .catch((error) => {
1292
+ throw this.projectExternalError(run, error, 'toolCleanup');
1293
+ }),
814
1294
  ]).then(([settled]) => settled);
815
1295
  const errorsBeforePersistence: unknown[] = [];
816
1296
  if (originalError !== undefined) errorsBeforePersistence.push(originalError);
@@ -848,7 +1328,12 @@ export class FlexHarness<TScope = unknown> {
848
1328
  )]).then(([settled]) => settled);
849
1329
 
850
1330
  if (persistenceResult.status === 'rejected') {
851
- const allErrors = [...errorsBeforePersistence, persistenceResult.reason];
1331
+ const persistenceError = this.projectExternalError(
1332
+ run,
1333
+ persistenceResult.reason,
1334
+ 'persistence',
1335
+ );
1336
+ const allErrors = [...errorsBeforePersistence, persistenceError];
852
1337
  const combinedError = combineErrors(allErrors);
853
1338
  const fallback = this.applyRunFinalState(
854
1339
  resolved.state,
@@ -860,14 +1345,18 @@ export class FlexHarness<TScope = unknown> {
860
1345
  combinedError,
861
1346
  cancelled,
862
1347
  );
863
- resolved.state.activeRuns.delete(run.sessionId);
1348
+ if (resolved.state.activeRuns.get(run.sessionId) === run) {
1349
+ resolved.state.activeRuns.delete(run.sessionId);
1350
+ }
864
1351
  this.emitFinalMutationEvents(run, fallback);
865
1352
  this.emitFinalRunEvent(run, fallback.assistantMessage, combinedError, cancelled);
866
1353
  throw combinedError;
867
1354
  }
868
1355
 
869
1356
  const finalized = persistenceResult.value;
870
- resolved.state.activeRuns.delete(run.sessionId);
1357
+ if (resolved.state.activeRuns.get(run.sessionId) === run) {
1358
+ resolved.state.activeRuns.delete(run.sessionId);
1359
+ }
871
1360
  this.emitFinalRunEvent(run, finalized.assistantMessage, terminalError, cancelled);
872
1361
  if (terminalError !== undefined) throw terminalError;
873
1362
  return {
@@ -945,22 +1434,25 @@ export class FlexHarness<TScope = unknown> {
945
1434
  run: IActiveRun,
946
1435
  prompt: INormalizedFlexPrompt,
947
1436
  model: IFlexResolvedModel | undefined,
948
- result: TFlexAgentRunResult | undefined,
1437
+ result: IRunResultProjection | undefined,
949
1438
  serializedResultMessages: TFlexAgentModelMessage[] | undefined,
950
1439
  originalError: unknown,
951
1440
  cancelled: boolean,
952
1441
  ): Promise<IFinalMutationResult> {
953
- const final = await this.mutateAndSave(state, () =>
954
- this.applyRunFinalState(
955
- state,
956
- run,
957
- prompt,
958
- model,
959
- result,
960
- serializedResultMessages,
961
- originalError,
962
- cancelled,
963
- ),
1442
+ const final = await this.mutateAndSave(
1443
+ state,
1444
+ () =>
1445
+ this.applyRunFinalState(
1446
+ state,
1447
+ run,
1448
+ prompt,
1449
+ model,
1450
+ result,
1451
+ serializedResultMessages,
1452
+ originalError,
1453
+ cancelled,
1454
+ ),
1455
+ true,
964
1456
  );
965
1457
  this.emitFinalMutationEvents(run, final);
966
1458
  return final;
@@ -971,7 +1463,7 @@ export class FlexHarness<TScope = unknown> {
971
1463
  run: IActiveRun,
972
1464
  prompt: INormalizedFlexPrompt,
973
1465
  model: IFlexResolvedModel | undefined,
974
- result: TFlexAgentRunResult | undefined,
1466
+ result: IRunResultProjection | undefined,
975
1467
  serializedResultMessages: TFlexAgentModelMessage[] | undefined,
976
1468
  originalError: unknown,
977
1469
  cancelled: boolean,
@@ -1190,16 +1682,19 @@ export class FlexHarness<TScope = unknown> {
1190
1682
  const output = event.success
1191
1683
  ? normalizeJsonValue(event.output, this.toolOutputLimits)
1192
1684
  : undefined;
1685
+ const projectedError = event.success
1686
+ ? undefined
1687
+ : this.projectExternalError(run, event.error, 'toolCallback');
1193
1688
  const byteLength = event.success
1194
1689
  ? Buffer.byteLength(JSON.stringify(output))
1195
- : Buffer.byteLength(event.error);
1690
+ : Buffer.byteLength(projectedError!.message);
1196
1691
  if (!this.reserveCallbackCapacity(run, 1, byteLength, 0)) return;
1197
1692
  if (event.success) {
1198
1693
  part.status = 'completed';
1199
1694
  part.output = output!;
1200
1695
  } else {
1201
1696
  part.status = 'failed';
1202
- part.error = event.error;
1697
+ part.error = projectedError!.message;
1203
1698
  }
1204
1699
  this.emitPartEvent(run, 'part.completed', part);
1205
1700
  }
@@ -1218,8 +1713,10 @@ export class FlexHarness<TScope = unknown> {
1218
1713
  nextOutputBytes > this.callbackLimits.maxOutputBytes ||
1219
1714
  nextParts > this.callbackLimits.maxParts
1220
1715
  ) {
1221
- const error = new FlexHarnessCallbackOverflowError(
1222
- `Callback buffer exceeded its limit (${nextEvents} events, ${nextOutputBytes} bytes, ${nextParts} parts).`,
1716
+ const error = Object.freeze(
1717
+ new FlexHarnessCallbackOverflowError(
1718
+ `Callback buffer exceeded its limit (${nextEvents} events, ${nextOutputBytes} bytes, ${nextParts} parts).`,
1719
+ ),
1223
1720
  );
1224
1721
  run.callbackError = error;
1225
1722
  this.abortRunInternally(run, error);
@@ -1308,7 +1805,7 @@ export class FlexHarness<TScope = unknown> {
1308
1805
  state.pendingPermissions.delete(request.permissionId);
1309
1806
  run.pendingPermissionIds.delete(request.permissionId);
1310
1807
  run.controller.signal.removeEventListener('abort', abortListener);
1311
- throw error;
1808
+ throw this.projectExternalError(run, error, 'persistence');
1312
1809
  }
1313
1810
  if (pending.settled) {
1314
1811
  return permissionPromise;
@@ -1344,49 +1841,67 @@ export class FlexHarness<TScope = unknown> {
1344
1841
  let addedRememberKey = false;
1345
1842
  pending.responding = true;
1346
1843
  try {
1347
- await this.mutateAndSave(state, () => {
1348
- const stored = this.requireSession(state, pending.request.sessionId);
1349
- if (decision === 'always' && rememberKey) {
1350
- addedRememberKey = !stored.rememberedPermissionKeys.has(rememberKey);
1351
- stored.rememberedPermissionKeys.add(rememberKey);
1352
- }
1353
- const hasOtherPending = [...state.pendingPermissions.values()].some(
1354
- (entry) =>
1355
- entry !== pending &&
1356
- !entry.settled &&
1357
- entry.request.sessionId === pending.request.sessionId,
1358
- );
1359
- stored.session.status = hasOtherPending ? 'waiting_permission' : 'running';
1360
- stored.session.activity.status = hasOtherPending ? 'waiting_permission' : 'running';
1361
- stored.session.updatedAt = new Date().toISOString();
1362
- });
1844
+ await this.mutateAndSave(
1845
+ state,
1846
+ () => {
1847
+ const stored = this.requireSession(state, pending.request.sessionId);
1848
+ if (decision === 'always' && rememberKey) {
1849
+ addedRememberKey = !stored.rememberedPermissionKeys.has(rememberKey);
1850
+ stored.rememberedPermissionKeys.add(rememberKey);
1851
+ }
1852
+ const hasOtherPending = [...state.pendingPermissions.values()].some(
1853
+ (entry) =>
1854
+ entry !== pending &&
1855
+ !entry.settled &&
1856
+ entry.request.sessionId === pending.request.sessionId,
1857
+ );
1858
+ stored.session.status = hasOtherPending ? 'waiting_permission' : 'running';
1859
+ stored.session.activity.status = hasOtherPending ? 'waiting_permission' : 'running';
1860
+ stored.session.updatedAt = new Date().toISOString();
1861
+ },
1862
+ true,
1863
+ );
1363
1864
  } catch (error) {
1865
+ const persistenceError = this.projectExternalError(pending.run, error, 'persistence');
1364
1866
  pending.responding = false;
1365
1867
  if (pending.abortReason !== undefined) {
1366
1868
  const abortReason = pending.abortReason;
1367
1869
  this.forceRejectPending(state, pending, abortReason);
1368
- throw combineErrors([abortReason, error]);
1870
+ throw combineErrors([abortReason, persistenceError]);
1369
1871
  }
1370
- throw error;
1872
+ throw persistenceError;
1371
1873
  }
1372
1874
 
1373
1875
  if (pending.abortReason !== undefined) {
1374
1876
  const abortReason = pending.abortReason;
1375
1877
  const rollbackPromise = addedRememberKey && rememberKey
1376
- ? this.mutateAndSave(state, () => {
1377
- this.requireSession(
1378
- state,
1379
- pending.request.sessionId,
1380
- ).rememberedPermissionKeys.delete(rememberKey);
1381
- })
1878
+ ? this.mutateAndSave(
1879
+ state,
1880
+ () => {
1881
+ this.requireSession(
1882
+ state,
1883
+ pending.request.sessionId,
1884
+ ).rememberedPermissionKeys.delete(rememberKey);
1885
+ },
1886
+ true,
1887
+ )
1382
1888
  : Promise.resolve();
1383
1889
  pending.responding = false;
1384
- this.forceRejectPending(state, pending, abortReason);
1385
1890
  try {
1386
1891
  await rollbackPromise;
1387
1892
  } catch (rollbackError) {
1388
- throw combineErrors([abortReason, rollbackError]);
1893
+ if (addedRememberKey && rememberKey) {
1894
+ state.sessions
1895
+ .get(pending.request.sessionId)
1896
+ ?.rememberedPermissionKeys.delete(rememberKey);
1897
+ }
1898
+ this.forceRejectPending(state, pending, abortReason);
1899
+ throw combineErrors([
1900
+ abortReason,
1901
+ this.projectExternalError(pending.run, rollbackError, 'persistence'),
1902
+ ]);
1389
1903
  }
1904
+ this.forceRejectPending(state, pending, abortReason);
1390
1905
  throw abortReason;
1391
1906
  }
1392
1907
 
@@ -1402,7 +1917,10 @@ export class FlexHarness<TScope = unknown> {
1402
1917
  decision,
1403
1918
  });
1404
1919
  if (decision === 'reject') {
1405
- pending.reject(new FlexHarnessPermissionRejectedError(pending.request.permissionId));
1920
+ const error = this.trustInternalError(
1921
+ new FlexHarnessPermissionRejectedError(pending.request.permissionId),
1922
+ );
1923
+ pending.reject(error);
1406
1924
  } else {
1407
1925
  pending.resolve();
1408
1926
  }
@@ -1460,19 +1978,102 @@ export class FlexHarness<TScope = unknown> {
1460
1978
  }
1461
1979
  }
1462
1980
 
1981
+ private projectExternalError(
1982
+ run: IActiveRun,
1983
+ error: unknown,
1984
+ source: TFlexExternalErrorSource,
1985
+ ): Error {
1986
+ if ((typeof error === 'object' && error !== null) || typeof error === 'function') {
1987
+ if (
1988
+ error === run.ownerCancellation
1989
+ || error === run.callbackError
1990
+ || this.trustedInternalErrors.has(error)
1991
+ ) {
1992
+ return error as Error;
1993
+ }
1994
+ }
1995
+ let info = externalErrorFallback;
1996
+ if (this.externalErrorProjector) {
1997
+ try {
1998
+ info = validateProjectedErrorInfo(this.externalErrorProjector(error, {
1999
+ source,
2000
+ scopeId: run.scopeId,
2001
+ sessionId: run.sessionId,
2002
+ runId: run.runId,
2003
+ }));
2004
+ } catch {
2005
+ info = externalErrorFallback;
2006
+ }
2007
+ }
2008
+ return this.trustInternalError(new FlexHarnessExternalError(info));
2009
+ }
2010
+
2011
+ private trustInternalError<TError extends Error>(error: TError): TError {
2012
+ Object.freeze(error);
2013
+ this.trustedInternalErrors.add(error);
2014
+ return error;
2015
+ }
2016
+
2017
+ private normalizeToolHandle(
2018
+ run: IActiveRun,
2019
+ toolHandle: IFlexToolHandle | undefined,
2020
+ ): IFlexToolHandle | undefined {
2021
+ if (toolHandle === undefined) return undefined;
2022
+ let close: IFlexToolHandle['close'];
2023
+ try {
2024
+ close = toolHandle.close;
2025
+ if (close !== undefined && typeof close !== 'function') {
2026
+ throw new FlexHarnessValidationError('Tool handle close must be a function.');
2027
+ }
2028
+ } catch (error) {
2029
+ throw this.projectExternalError(run, error, 'toolProvider');
2030
+ }
2031
+ const closeHandle = close === undefined ? undefined : () => close.call(toolHandle);
2032
+ try {
2033
+ const tools = toolHandle.tools;
2034
+ return {
2035
+ tools,
2036
+ ...(closeHandle === undefined ? {} : { close: closeHandle }),
2037
+ };
2038
+ } catch (error) {
2039
+ if (closeHandle) this.trackDetachedToolCleanup(closeHandle, run);
2040
+ throw this.projectExternalError(run, error, 'toolProvider');
2041
+ }
2042
+ }
2043
+
2044
+ private trackDetachedToolCleanup(
2045
+ cleanup: () => Promise<void> | void,
2046
+ run: IActiveRun,
2047
+ ): void {
2048
+ const record = { state: run.state } as IDetachedToolCleanup;
2049
+ record.completion = Promise.resolve()
2050
+ .then(cleanup)
2051
+ .catch((error) => {
2052
+ if (run.state.detachedToolCleanupErrors.length < maxDetachedCleanupErrors) {
2053
+ run.state.detachedToolCleanupErrors.push(
2054
+ this.projectExternalError(run, error, 'toolCleanup'),
2055
+ );
2056
+ }
2057
+ })
2058
+ .finally(() => {
2059
+ this.detachedToolCleanups.delete(record);
2060
+ });
2061
+ this.detachedToolCleanups.add(record);
2062
+ }
2063
+
1463
2064
  private observeDetachedToolProvider(
1464
2065
  outcomePromise: Promise<IResolverOutcome<IFlexToolHandle | undefined>>,
2066
+ run: IActiveRun,
1465
2067
  ): void {
1466
- const cleanup = outcomePromise.then(async (outcome) => {
1467
- if (outcome.success && outcome.source === 'tools' && outcome.value?.close) {
1468
- await Promise.resolve().then(() => outcome.value!.close!());
2068
+ this.trackDetachedToolCleanup(async () => {
2069
+ const outcome = await outcomePromise;
2070
+ if (!outcome.success || outcome.source !== 'tools' || !outcome.value) return;
2071
+ const close = outcome.value.close;
2072
+ if (close !== undefined && typeof close !== 'function') {
2073
+ throw new FlexHarnessValidationError('Tool handle close must be a function.');
1469
2074
  }
1470
- });
1471
- this.detachedToolCleanups.add(cleanup);
1472
- void cleanup.then(
1473
- () => this.detachedToolCleanups.delete(cleanup),
1474
- () => undefined,
1475
- );
2075
+ await close?.call(outcome.value);
2076
+ }, run);
1476
2077
  }
1477
2078
 
1478
2079
  private emitEvent(scopeId: string, sessionId: string, details: TEventDetails): void {
@@ -1495,27 +2096,188 @@ export class FlexHarness<TScope = unknown> {
1495
2096
  }
1496
2097
  }
1497
2098
 
1498
- private async resolveState(
1499
- scopeId: string,
1500
- ): Promise<{ scope: IFlexResolvedScope<TScope>; state: IStorageState }> {
1501
- this.assertOpen();
1502
- validateIdentifier(scopeId, 'scopeId');
2099
+ private getScopeAdmission(scopeId: string): IScopeAdmissionState {
2100
+ let admission = this.scopeAdmissions.get(scopeId);
2101
+ if (!admission) {
2102
+ admission = { generation: 0, inFlightResolvers: 0, retiring: false };
2103
+ this.scopeAdmissions.set(scopeId, admission);
2104
+ }
2105
+ return admission;
2106
+ }
2107
+
2108
+ private pruneScopeAdmission(scopeId: string, admission: IScopeAdmissionState): void {
2109
+ if (
2110
+ !admission.retiring
2111
+ && admission.inFlightResolvers === 0
2112
+ && this.scopeAdmissions.get(scopeId) === admission
2113
+ ) {
2114
+ this.scopeAdmissions.delete(scopeId);
2115
+ }
2116
+ }
2117
+
2118
+ private async retireScopeInternal(scopeId: string): Promise<void> {
1503
2119
  const scope = await this.scopeResolver.resolveScope(scopeId);
1504
2120
  this.assertOpen();
1505
2121
  validateIdentifier(scope.storageKey, 'resolved storageKey');
1506
- let stateLoad = this.stateLoads.get(scope.storageKey);
2122
+ const stateLoad = this.stateLoads.get(scope.storageKey);
1507
2123
  if (!stateLoad) {
1508
- stateLoad = this.loadState(scope.storageKey);
1509
- this.stateLoads.set(scope.storageKey, stateLoad);
1510
- void stateLoad.catch(() => {
1511
- if (this.stateLoads.get(scope.storageKey) === stateLoad) {
1512
- this.stateLoads.delete(scope.storageKey);
2124
+ const existingDrain = this.storageDrains.get(scope.storageKey);
2125
+ if (existingDrain) await existingDrain;
2126
+ return;
2127
+ }
2128
+ await this.drainStorage(
2129
+ scope.storageKey,
2130
+ stateLoad,
2131
+ this.createScopeRetirementError(),
2132
+ );
2133
+ }
2134
+
2135
+ private drainStorage(
2136
+ storageKey: string,
2137
+ stateLoad: Promise<IStorageState>,
2138
+ abortReason: FlexHarnessAbortError,
2139
+ ): Promise<void> {
2140
+ const existingDrain = this.storageDrains.get(storageKey);
2141
+ if (existingDrain) return existingDrain;
2142
+ let drain!: Promise<void>;
2143
+ drain = this.drainStorageInternal(stateLoad, abortReason).finally(() => {
2144
+ if (this.stateLoads.get(storageKey) === stateLoad) {
2145
+ this.stateLoads.delete(storageKey);
2146
+ }
2147
+ if (this.storageDrains.get(storageKey) === drain) {
2148
+ this.storageDrains.delete(storageKey);
2149
+ }
2150
+ });
2151
+ this.storageDrains.set(storageKey, drain);
2152
+ return drain;
2153
+ }
2154
+
2155
+ private async drainStorageInternal(
2156
+ stateLoad: Promise<IStorageState>,
2157
+ abortReason: FlexHarnessAbortError,
2158
+ ): Promise<void> {
2159
+ let state: IStorageState;
2160
+ try {
2161
+ state = await stateLoad;
2162
+ } catch {
2163
+ return;
2164
+ }
2165
+
2166
+ const finalizers: Promise<IFlexPromptResult>[] = [];
2167
+ try {
2168
+ state.lifecycle = 'retiring';
2169
+ for (const run of state.activeRuns.values()) {
2170
+ finalizers.push(run.finalizer);
2171
+ if (run.phase === 'committing') continue;
2172
+ if (run.internalFailure === undefined) run.ownerCancellation ??= abortReason;
2173
+ this.rejectRunPermissions(state, run, abortReason);
2174
+ run.controller.abort(abortReason);
2175
+ }
2176
+ for (const pending of [...state.pendingPermissions.values()]) {
2177
+ this.rejectPending(state, pending, abortReason);
2178
+ }
2179
+
2180
+ const results = await Promise.allSettled(finalizers);
2181
+ await state.saveTail;
2182
+ await this.drainDetachedToolCleanups(state);
2183
+ const unexpectedErrors: unknown[] = [];
2184
+ for (const result of results) {
2185
+ if (result.status === 'rejected') {
2186
+ this.appendUnexpectedErrors(unexpectedErrors, result.reason);
1513
2187
  }
1514
- });
2188
+ }
2189
+ unexpectedErrors.push(...state.detachedToolCleanupErrors.splice(0));
2190
+ if (unexpectedErrors.length > 0) {
2191
+ throw combineErrors(unexpectedErrors);
2192
+ }
2193
+ } finally {
2194
+ state.lifecycle = 'retired';
2195
+ state.sessions.clear();
2196
+ state.activeRuns.clear();
2197
+ state.pendingPermissions.clear();
2198
+ state.detachedToolCleanupErrors.length = 0;
2199
+ }
2200
+ }
2201
+
2202
+ private async drainDetachedToolCleanups(state: IStorageState): Promise<void> {
2203
+ while (true) {
2204
+ const cleanups = [...this.detachedToolCleanups]
2205
+ .filter((cleanup) => cleanup.state === state)
2206
+ .map((cleanup) => cleanup.completion);
2207
+ if (cleanups.length === 0) return;
2208
+ await Promise.all(cleanups);
2209
+ }
2210
+ }
2211
+
2212
+ private appendUnexpectedErrors(target: unknown[], error: unknown): void {
2213
+ if (error instanceof FlexHarnessRunError) {
2214
+ for (const nestedError of error.errors) {
2215
+ this.appendUnexpectedErrors(target, nestedError);
2216
+ }
2217
+ } else if (!isAbortError(error)) {
2218
+ target.push(error);
2219
+ }
2220
+ }
2221
+
2222
+ private async waitForReadableState(state: IStorageState): Promise<void> {
2223
+ await state.saveTail;
2224
+ this.assertStateAcceptingWork(state);
2225
+ }
2226
+
2227
+ private assertStateAcceptingWork(state: IStorageState): void {
2228
+ if (state.lifecycle !== 'active' || this.storageDrains.has(state.storageKey)) {
2229
+ throw this.createScopeRetirementError();
1515
2230
  }
1516
- const state = await stateLoad;
2231
+ }
2232
+
2233
+ private createScopeRetirementError(): FlexHarnessAbortError {
2234
+ return this.trustInternalError(new FlexHarnessAbortError(scopeRetirementMessage));
2235
+ }
2236
+
2237
+ private async resolveState(
2238
+ scopeId: string,
2239
+ ): Promise<{ scope: IFlexResolvedScope<TScope>; state: IStorageState }> {
1517
2240
  this.assertOpen();
1518
- return { scope, state };
2241
+ validateIdentifier(scopeId, 'scopeId');
2242
+ const admission = this.getScopeAdmission(scopeId);
2243
+ if (admission.retiring) throw this.createScopeRetirementError();
2244
+ const generation = admission.generation;
2245
+ admission.inFlightResolvers++;
2246
+ try {
2247
+ const scope = await this.scopeResolver.resolveScope(scopeId);
2248
+ this.assertOpen();
2249
+ if (admission.retiring || admission.generation !== generation) {
2250
+ throw this.createScopeRetirementError();
2251
+ }
2252
+ validateIdentifier(scope.storageKey, 'resolved storageKey');
2253
+ if (this.storageDrains.has(scope.storageKey)) {
2254
+ throw this.createScopeRetirementError();
2255
+ }
2256
+ let stateLoad = this.stateLoads.get(scope.storageKey);
2257
+ if (!stateLoad) {
2258
+ stateLoad = this.loadState(scope.storageKey);
2259
+ this.stateLoads.set(scope.storageKey, stateLoad);
2260
+ void stateLoad.catch(() => {
2261
+ if (this.stateLoads.get(scope.storageKey) === stateLoad) {
2262
+ this.stateLoads.delete(scope.storageKey);
2263
+ }
2264
+ });
2265
+ }
2266
+ const state = await stateLoad;
2267
+ this.assertOpen();
2268
+ if (
2269
+ admission.retiring
2270
+ || admission.generation !== generation
2271
+ || this.storageDrains.has(scope.storageKey)
2272
+ || state.lifecycle !== 'active'
2273
+ ) {
2274
+ throw this.createScopeRetirementError();
2275
+ }
2276
+ return { scope, state };
2277
+ } finally {
2278
+ admission.inFlightResolvers--;
2279
+ this.pruneScopeAdmission(scopeId, admission);
2280
+ }
1519
2281
  }
1520
2282
 
1521
2283
  private async loadState(storageKey: string): Promise<IStorageState> {
@@ -1560,11 +2322,18 @@ export class FlexHarness<TScope = unknown> {
1560
2322
  activeRuns: new Map<string, IActiveRun>(),
1561
2323
  pendingPermissions: new Map<string, IPendingPermission>(),
1562
2324
  saveTail: Promise.resolve(),
2325
+ lifecycle: 'active',
2326
+ detachedToolCleanupErrors: [],
1563
2327
  };
1564
2328
  }
1565
2329
 
1566
- private mutateAndSave<T>(state: IStorageState, mutation: () => T): Promise<T> {
2330
+ private mutateAndSave<T>(
2331
+ state: IStorageState,
2332
+ mutation: () => T,
2333
+ allowDuringDrain = false,
2334
+ ): Promise<T> {
1567
2335
  const operation = state.saveTail.then(async () => {
2336
+ if (!allowDuringDrain) this.assertStateAcceptingWork(state);
1568
2337
  const beforeMutation = this.createSnapshot(state, state.revision);
1569
2338
  try {
1570
2339
  const result = mutation();
@@ -1615,6 +2384,76 @@ export class FlexHarness<TScope = unknown> {
1615
2384
  };
1616
2385
  }
1617
2386
 
2387
+ private messageCursorNamespace(storageKey: string): string {
2388
+ return plugins.crypto.createHash('sha256').update(storageKey).digest('base64url');
2389
+ }
2390
+
2391
+ private createMessageCursor(
2392
+ namespace: string,
2393
+ sessionId: string,
2394
+ anchorMessageId: string,
2395
+ ): string {
2396
+ const cursor = Buffer.from(JSON.stringify({
2397
+ version: 1,
2398
+ namespace,
2399
+ sessionId,
2400
+ anchorMessageId,
2401
+ } satisfies IFlexMessageCursor)).toString('base64url');
2402
+ if (Buffer.byteLength(cursor, 'utf8') > maxMessagePageCursorBytes) {
2403
+ throw new FlexHarnessValidationError('Message page cursor exceeds its transfer limit.');
2404
+ }
2405
+ return cursor;
2406
+ }
2407
+
2408
+ private parseMessageCursor(cursor: string): IFlexMessageCursor {
2409
+ let parsed: unknown;
2410
+ try {
2411
+ const decoded = Buffer.from(cursor, 'base64url');
2412
+ if (decoded.toString('base64url') !== cursor) {
2413
+ throw new Error('non-canonical cursor');
2414
+ }
2415
+ parsed = JSON.parse(decoded.toString('utf8'));
2416
+ } catch {
2417
+ throw new FlexHarnessValidationError('Message page cursor is invalid.');
2418
+ }
2419
+ if (
2420
+ !parsed
2421
+ || typeof parsed !== 'object'
2422
+ || Array.isArray(parsed)
2423
+ || Object.keys(parsed).length !== 4
2424
+ || (parsed as Partial<IFlexMessageCursor>).version !== 1
2425
+ || typeof (parsed as Partial<IFlexMessageCursor>).namespace !== 'string'
2426
+ || typeof (parsed as Partial<IFlexMessageCursor>).sessionId !== 'string'
2427
+ || typeof (parsed as Partial<IFlexMessageCursor>).anchorMessageId !== 'string'
2428
+ ) {
2429
+ throw new FlexHarnessValidationError('Message page cursor is invalid.');
2430
+ }
2431
+ const result = parsed as IFlexMessageCursor;
2432
+ if (
2433
+ result.namespace.length === 0
2434
+ || result.sessionId.length === 0
2435
+ || result.anchorMessageId.length === 0
2436
+ || Buffer.byteLength(result.namespace, 'utf8') > 128
2437
+ || Buffer.byteLength(result.sessionId, 'utf8') > maxTransferIdentifierBytes
2438
+ || Buffer.byteLength(result.anchorMessageId, 'utf8') > maxTransferIdentifierBytes
2439
+ ) {
2440
+ throw new FlexHarnessValidationError('Message page cursor is invalid.');
2441
+ }
2442
+ return result;
2443
+ }
2444
+
2445
+ private resolveRunAdmission(run: IActiveRun): void {
2446
+ if (run.admissionSettled) return;
2447
+ run.admissionSettled = true;
2448
+ run.resolveAdmission();
2449
+ }
2450
+
2451
+ private rejectRunAdmission(run: IActiveRun, error: unknown): void {
2452
+ if (run.admissionSettled) return;
2453
+ run.admissionSettled = true;
2454
+ run.rejectAdmission(error);
2455
+ }
2456
+
1618
2457
  private requireSession(state: IStorageState, sessionId: string): IStoredSessionState {
1619
2458
  validateIdentifier(sessionId, 'sessionId');
1620
2459
  const session = state.sessions.get(sessionId);