@modelprofile.com/flexharness 10.11.0 → 10.13.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.
@@ -14,6 +14,7 @@ import {
14
14
  FlexHarnessSlashCommandReentryError,
15
15
  FlexHarnessSlashCommandUnavailableError,
16
16
  FlexHarnessSteerRejectedError,
17
+ FlexHarnessStepLimitError,
17
18
  FlexHarnessStoreConflictError,
18
19
  FlexHarnessStoreCommitUncertainError,
19
20
  FlexHarnessValidationError,
@@ -28,7 +29,13 @@ import {
28
29
  FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES,
29
30
  FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES,
30
31
  FLEX_SESSION_IMPORT_MAX_DEPTH,
32
+ FLEX_TOOL_CATALOG_TOOL_NAMES,
31
33
  } from './interfaces.js';
34
+ import {
35
+ FlexToolCatalog,
36
+ assertNoCatalogCollisions,
37
+ catalogToolInputSchema,
38
+ } from './classes.toolcatalog.js';
32
39
  import type {
33
40
  IFlexAgentContextCompactionOptions,
34
41
  IFlexAgentContextInvocation,
@@ -95,6 +102,8 @@ import type {
95
102
  IFlexSessionPermissionPolicyContext,
96
103
  IFlexSessionActivity,
97
104
  IFlexSessionGeneration,
105
+ IFlexSessionMetrics,
106
+ IFlexSessionMetricsState,
98
107
  IFlexSessionGenerationCohortEntry,
99
108
  IFlexSessionImportHandle,
100
109
  IFlexSessionImportLimits,
@@ -109,6 +118,9 @@ import type {
109
118
  IFlexSubagentDefinition,
110
119
  IFlexSubagentProvenance,
111
120
  IFlexTerminalProjection,
121
+ IFlexCatalogDescribeInput,
122
+ IFlexCatalogSearchInput,
123
+ IFlexToolCatalogOptions,
112
124
  IFlexToolHandle,
113
125
  IFlexToolMessagePart,
114
126
  IFlexToolProviderContext,
@@ -165,6 +177,7 @@ import {
165
177
  normalizeAgentSessionPolicy,
166
178
  normalizeBuiltInTools,
167
179
  normalizeSlashCommands,
180
+ normalizeToolCatalogOptions,
168
181
  normalizeSubagents,
169
182
  requireHarnessStores,
170
183
  resolveBoundedPositiveInteger,
@@ -226,6 +239,15 @@ import {
226
239
  validateUtf8String,
227
240
  } from './utils.validation.js';
228
241
 
242
+ import {
243
+ createSessionMetricsOperation,
244
+ createSessionMetricsState,
245
+ observeSessionMetricsCall,
246
+ sessionMetricsView,
247
+ settleSessionMetricsOperation,
248
+ type ISessionMetricsOperation,
249
+ } from './utils.sessionmetrics.js';
250
+
229
251
  type TCanonicalOutcome = 'accepted' | 'rejected' | 'interrupted';
230
252
  type TRunPhase =
231
253
  | 'admitting'
@@ -256,6 +278,10 @@ interface IStoredSessionState {
256
278
  projectionReconciliationRequired: boolean;
257
279
  projectionRevision: number;
258
280
  projectionQueue: Promise<void>;
281
+ metrics: IFlexSessionMetricsState;
282
+ metricsCompaction?: ISessionMetricsOperation;
283
+ /** Retained independently of the error-neutral projectionQueue. */
284
+ metricsFailure?: unknown;
259
285
  rememberedPermissionKeys: Set<string>;
260
286
  permissionRevision: number;
261
287
  permissionQueue: Promise<void>;
@@ -402,17 +428,23 @@ interface IActiveRun {
402
428
  finishedEventEmitted: boolean;
403
429
  /** The usage the provider reported for the run's model calls so far. */
404
430
  usage: IFlexUsage;
431
+ metricsOperation?: ISessionMetricsOperation;
405
432
  reasoningPartIds: Map<string, string>;
406
433
  toolPartIds: Map<string, string>;
407
434
  trustedToolExecutionErrors: Map<string, Error>;
408
435
  subagentCallCount: number;
409
436
  subagentSessionIds: Set<string>;
410
437
  backgroundSubagents: Set<Promise<void>>;
438
+ cacheDispatch?: plugins.ISmartAiCacheDispatch;
411
439
  pendingPermissionIds: Set<string>;
412
440
  phase: TRunPhase;
413
441
  reservedUserMessage?: IFlexMessage;
414
442
  reservedAssistantMessage?: IFlexMessage;
415
443
  modelResolution?: IFlexResolvedModel;
444
+ /** The tool-provider context of the run, passed to catalog methods. */
445
+ toolProviderContext?: Readonly<IFlexToolProviderContext<unknown>>;
446
+ /** The catalog methods the run offers through search_tools, describe_tools and execute. */
447
+ toolCatalog?: FlexToolCatalog<unknown>;
416
448
  subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
417
449
  allowedTools?: readonly string[];
418
450
  agentAllowedTools?: readonly string[];
@@ -466,6 +498,7 @@ interface IDelegatedRunAdmissionSeed {
466
498
  readonly parentQueueId: string;
467
499
  readonly parentRunId: string;
468
500
  readonly parentToolCallId: string;
501
+ readonly configurationRef?: string;
469
502
  readonly agent: string;
470
503
  readonly depth: number;
471
504
  }
@@ -672,7 +705,7 @@ const maxSubagentPromptBytes = 64 * 1024;
672
705
  const maxSubagentTaskIdBytes = 512;
673
706
  const maxSubagentPermissionMetadataBytes = 16 * 1024;
674
707
  const maxSubagentResultTextBytes = 64 * 1024;
675
- const defaultMaxSubagentDepth = 1;
708
+ const defaultMaxSubagentDepth = 6;
676
709
  const maximumMaxSubagentDepth = 8;
677
710
  const defaultMaxSubagentCallsPerRun = 32;
678
711
  const maximumMaxSubagentCallsPerRun = 128;
@@ -832,6 +865,7 @@ export class FlexHarness<TScope = unknown> {
832
865
  private readonly externalErrorProjector?: TFlexExternalErrorProjector;
833
866
  private readonly subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
834
867
  private readonly builtInTools: INormalizedBuiltInTools;
868
+ private readonly toolCatalogOptions: Readonly<Required<IFlexToolCatalogOptions>>;
835
869
  private readonly slashCommands: ReadonlyMap<string, TRegisteredSlashCommand<TScope>>;
836
870
  private readonly maxSubagentDepth: number;
837
871
  private readonly maxSubagentCallsPerRun: number;
@@ -918,6 +952,7 @@ export class FlexHarness<TScope = unknown> {
918
952
  this.externalErrorProjector = options.externalErrorProjector;
919
953
  this.subagents = normalizeSubagents(options.subagents);
920
954
  this.builtInTools = normalizeBuiltInTools(options.builtInTools);
955
+ this.toolCatalogOptions = normalizeToolCatalogOptions(options.toolCatalog);
921
956
  this.slashCommands = normalizeSlashCommands(options.slashCommands);
922
957
  this.maxSubagentDepth = resolveBoundedPositiveInteger(
923
958
  options.maxSubagentDepth,
@@ -1029,6 +1064,7 @@ export class FlexHarness<TScope = unknown> {
1029
1064
  scopeId,
1030
1065
  resolved.scope.scope,
1031
1066
  placeholder.compactorLifecycleController,
1067
+ true,
1032
1068
  );
1033
1069
  const loaded = await initialization;
1034
1070
  if (resolved.state.sessions.get(sessionId) !== placeholder) {
@@ -1314,6 +1350,7 @@ export class FlexHarness<TScope = unknown> {
1314
1350
  await this.stores.projections.save(state.storageKey, sessionId, {
1315
1351
  schemaVersion: 3,
1316
1352
  revision: 1,
1353
+ metrics: createSessionMetricsState(false),
1317
1354
  messages: staged.runs.flatMap((run) => [run.user, run.assistant]),
1318
1355
  stagedTerminals: [],
1319
1356
  reversionSegments: [],
@@ -1390,6 +1427,16 @@ export class FlexHarness<TScope = unknown> {
1390
1427
  return publicSnapshot(this.requireSession(state, sessionId).session);
1391
1428
  }
1392
1429
 
1430
+ /** Bounded projection-state read: no message, event, archive or child-history scan. */
1431
+ public async getSessionMetrics(scopeId: string, sessionId: string): Promise<IFlexSessionMetrics> {
1432
+ const { state } = await this.resolveState(scopeId);
1433
+ const stored = this.requireSession(state, sessionId);
1434
+ await stored.projectionQueue;
1435
+ this.assertStateAcceptingWork(state);
1436
+ if (stored.metricsFailure !== undefined) throw stored.metricsFailure;
1437
+ return publicSnapshot(sessionMetricsView(stored.metrics));
1438
+ }
1439
+
1393
1440
  public async getSessionReversionInfo(
1394
1441
  scopeId: string,
1395
1442
  sessionId: string,
@@ -3014,13 +3061,53 @@ export class FlexHarness<TScope = unknown> {
3014
3061
  }
3015
3062
  throw new FlexHarnessNotFoundError('Prompt queue entry', queueId);
3016
3063
  }
3064
+ return this.admitRunSteer(run, steer, prompt, byteSize);
3065
+ }
3066
+
3067
+ /** Sends follow-up input only to an exact running direct child owned by this parent run. */
3068
+ public async sendSubagentInput(
3069
+ scopeId: string,
3070
+ parentSessionId: string,
3071
+ parentRunId: string,
3072
+ taskId: string,
3073
+ steer: IFlexSteerInput,
3074
+ ): Promise<IFlexSteerAdmission> {
3075
+ validateIdentifier(parentSessionId, 'parentSessionId');
3076
+ requireTransferIdentifier(parentSessionId, 'parentSessionId');
3077
+ validateIdentifier(parentRunId, 'parentRunId');
3078
+ requireTransferIdentifier(parentRunId, 'parentRunId');
3079
+ this.assertOpen();
3080
+ const { state } = await this.resolveState(scopeId);
3081
+ const parent = state.activeRuns.get(parentSessionId);
3082
+ if (!parent || parent.runId !== parentRunId) {
3083
+ throw new FlexHarnessValidationError('The exact parent run is not active.');
3084
+ }
3085
+ return this.sendSubagentInputInRun(parent, taskId, steer);
3086
+ }
3087
+
3088
+ /** The shared synchronous admission point; neither entrypoint starts or resumes a run. */
3089
+ private admitRunSteer(
3090
+ run: IActiveRun,
3091
+ steer: IFlexSteerInput,
3092
+ prompt: INormalizedFlexPrompt,
3093
+ byteSize: number,
3094
+ ): IFlexSteerAdmission {
3095
+ if (
3096
+ run.phase === 'closing'
3097
+ || run.phase === 'finalizing'
3098
+ || run.phase === 'promoting'
3099
+ || run.callbacksClosed
3100
+ || run.controller.signal.aborted
3101
+ || run.stop.signal.aborted
3102
+ || run.state.activeRuns.get(run.sessionId) !== run
3103
+ ) throw new FlexHarnessSteerRejectedError('not-running', steer.steerId);
3017
3104
  const steering = run.steering;
3018
3105
  if (!steering.input.closed && !steering.accepted.has(steer.steerId)) {
3019
3106
  if (
3020
3107
  steering.accepted.size - steering.applied.size
3021
3108
  >= this.promptQueueLimits.maxOutstandingPromptsPerSession
3022
3109
  ) {
3023
- throw new FlexHarnessQueueFullError(`Session "${sessionId}" has reached its pending steer limit.`);
3110
+ throw new FlexHarnessQueueFullError(`Session "${run.sessionId}" has reached its pending steer limit.`);
3024
3111
  }
3025
3112
  // Pending steers hold outstanding prompt bytes of the session until they are taken in.
3026
3113
  if (
@@ -3028,7 +3115,7 @@ export class FlexHarness<TScope = unknown> {
3028
3115
  > this.promptQueueLimits.maxOutstandingBytesPerSession
3029
3116
  ) {
3030
3117
  throw new FlexHarnessQueueFullError(
3031
- `Session "${sessionId}" has reached its outstanding prompt byte limit.`,
3118
+ `Session "${run.sessionId}" has reached its outstanding prompt byte limit.`,
3032
3119
  );
3033
3120
  }
3034
3121
  }
@@ -3052,11 +3139,130 @@ export class FlexHarness<TScope = unknown> {
3052
3139
  steering.stored.outstandingPromptBytes += byteSize;
3053
3140
  this.emitEvent(run.scopeId, run.stored.session, {
3054
3141
  type: 'prompt.steer.accepted',
3055
- queueId,
3142
+ queueId: run.queueId,
3056
3143
  runId: run.runId,
3057
3144
  steerId: steer.steerId,
3058
3145
  });
3059
- return { steerId: steer.steerId, queueId, runId: run.runId };
3146
+ return { steerId: steer.steerId, queueId: run.queueId, runId: run.runId };
3147
+ }
3148
+
3149
+ private sendSubagentInputInRun(
3150
+ parent: IActiveRun,
3151
+ taskId: string,
3152
+ steer: IFlexSteerInput,
3153
+ ): IFlexSteerAdmission {
3154
+ this.assertOpen();
3155
+ validateUtf8String(taskId, 'taskId', maxSubagentTaskIdBytes, true);
3156
+ validateIdentifier(steer.steerId, 'steerId');
3157
+ requireTransferIdentifier(steer.steerId, 'steerId');
3158
+ const prompt = normalizeFlexPrompt(steer.prompt);
3159
+ const byteSize = this.promptAdmissionByteSize(steer.prompt, {});
3160
+ this.assertStateAcceptingWork(parent.state);
3161
+ const child = parent.state.activeRuns.get(taskId);
3162
+ if (!child) throw new FlexHarnessSteerRejectedError('not-running', steer.steerId);
3163
+ this.assertLiveSubagentOwnership(child, parent);
3164
+ return this.admitRunSteer(child, steer, prompt, byteSize);
3165
+ }
3166
+
3167
+ /** Fences live ownership by exact runtime pointers and admission seeds all the way to root. */
3168
+ private assertLiveSubagentOwnership(child: IActiveRun, parent: IActiveRun): void {
3169
+ const state = parent.state;
3170
+ const visited = new Set<IActiveRun>();
3171
+ let current = child;
3172
+ let expectedParent: IActiveRun | undefined = parent;
3173
+ while (true) {
3174
+ const stored = current.stored;
3175
+ const session = stored.session;
3176
+ const queued = stored.outstandingPromptsById.get(current.queueId);
3177
+ if (
3178
+ visited.has(current)
3179
+ || current.state !== state
3180
+ || state.activeRuns.get(current.sessionId) !== current
3181
+ || state.sessions.get(current.sessionId) !== stored
3182
+ || session.sessionId !== current.sessionId
3183
+ || state.tombstones.has(current.sessionId)
3184
+ || state.initializingSessions.has(current.sessionId)
3185
+ || current.phase !== 'running'
3186
+ || current.callbacksClosed
3187
+ || current.controller.signal.aborted
3188
+ || current.stop.signal.aborted
3189
+ || current.steering.input.closed
3190
+ || !queued
3191
+ || queued.state !== state
3192
+ || queued.stored !== stored
3193
+ || queued.sessionId !== current.sessionId
3194
+ || queued.queueId !== current.queueId
3195
+ || queued.runId !== current.runId
3196
+ || queued.steering !== current.steering
3197
+ ) throw new FlexHarnessValidationError('The subagent or ancestor run is no longer live and owned.');
3198
+ this.assertSessionWritable(stored);
3199
+ visited.add(current);
3200
+ if (session.parentSessionId === undefined) {
3201
+ if (expectedParent !== undefined || queued.delegationSeed !== undefined) {
3202
+ throw new FlexHarnessValidationError('The task is not a direct child of this parent run.');
3203
+ }
3204
+ return;
3205
+ }
3206
+ const seed = queued.delegationSeed;
3207
+ if (!seed) throw new FlexHarnessValidationError('The child has no live delegated admission owner.');
3208
+ const owner = current.delegatedRunAdmissionOwner;
3209
+ const owningParent = seed.parentRun;
3210
+ const parentSession = owningParent.stored.session;
3211
+ if (
3212
+ (expectedParent !== undefined && owningParent !== expectedParent)
3213
+ || seed.state !== state
3214
+ || seed.storageKey !== state.storageKey
3215
+ || seed.childStored !== stored
3216
+ || stored.storageKey !== seed.storageKey
3217
+ || seed.sessionId !== current.sessionId
3218
+ || session.sessionId !== seed.sessionId
3219
+ || session.sessionGenerationId !== seed.sessionGenerationId
3220
+ || session.sessionGenerationSequence !== seed.sessionGenerationSequence
3221
+ || session.parentSessionId !== seed.parentSessionId
3222
+ || session.parentRunId !== seed.originParentRunId
3223
+ || session.parentToolCallId !== seed.originParentToolCallId
3224
+ || session.agent !== seed.agent
3225
+ || session.depth !== seed.depth
3226
+ || session.configurationRef !== seed.configurationRef
3227
+ || parentSession.configurationRef !== seed.configurationRef
3228
+ || seed.depth !== (parentSession.depth ?? 0) + 1
3229
+ || !owningParent.subagents.has(seed.agent)
3230
+ || seed.parentStored !== owningParent.stored
3231
+ || seed.parentStored.storageKey !== seed.storageKey
3232
+ || owningParent.sessionId !== seed.parentSessionId
3233
+ || owningParent.runId !== seed.parentRunId
3234
+ || owningParent.queueId !== seed.parentQueueId
3235
+ || parentSession.sessionGenerationId !== seed.parentSessionGenerationId
3236
+ || parentSession.sessionGenerationSequence !== seed.parentSessionGenerationSequence
3237
+ || current.scopeId !== seed.scopeId
3238
+ || current.scope !== seed.scope
3239
+ || owningParent.scopeId !== seed.scopeId
3240
+ || owningParent.scope !== seed.scope
3241
+ || (this.delegatedRunAdmissionProvider !== undefined && owner === undefined)
3242
+ || (owner !== undefined && (
3243
+ owner.storageState !== state || owner.run !== current
3244
+ || !state.delegatedRunAdmissionOwners.has(owner)
3245
+ || owner.status !== 'acquired' || !owner.acquisitionSettled || owner.acquisitionDetached
3246
+ || owner.context.runId !== current.runId || owner.context.queueId !== current.queueId
3247
+ || owner.context.parentRunId !== owningParent.runId
3248
+ || owner.context.parentQueueId !== owningParent.queueId
3249
+ || owner.context.scopeId !== seed.scopeId || owner.context.scope !== seed.scope
3250
+ || owner.context.storageKey !== seed.storageKey
3251
+ || owner.context.sessionId !== seed.sessionId
3252
+ || owner.context.parentSessionId !== seed.parentSessionId
3253
+ || owner.context.parentToolCallId !== seed.parentToolCallId
3254
+ || owner.context.originParentRunId !== seed.originParentRunId
3255
+ || owner.context.originParentToolCallId !== seed.originParentToolCallId
3256
+ || owner.context.agent !== seed.agent || owner.context.depth !== seed.depth
3257
+ || owner.context.sessionGenerationId !== seed.sessionGenerationId
3258
+ || owner.context.sessionGenerationSequence !== seed.sessionGenerationSequence
3259
+ || owner.context.parentSessionGenerationId !== seed.parentSessionGenerationId
3260
+ || owner.context.parentSessionGenerationSequence !== seed.parentSessionGenerationSequence
3261
+ ))
3262
+ ) throw new FlexHarnessValidationError('The task is not owned by this exact parent run and configuration.');
3263
+ current = owningParent;
3264
+ expectedParent = undefined;
3265
+ }
3060
3266
  }
3061
3267
 
3062
3268
  public async abort(scopeId: string, sessionId: string): Promise<boolean> {
@@ -4532,7 +4738,12 @@ export class FlexHarness<TScope = unknown> {
4532
4738
  this.assertPromptPromotion(queued, run);
4533
4739
  const reservation = this.createReservation(run, prompt);
4534
4740
  closeReservationPublication = this.openPublication(run.state, run.sessionId);
4741
+ run.metricsOperation = createSessionMetricsOperation(run.runId, 'generation');
4535
4742
  await this.mutateProjection(run.state, run.stored, () => {
4743
+ if (run.stored.metrics.pendingGenerationId !== undefined) {
4744
+ throw new Error('Session metrics already owns a generation.');
4745
+ }
4746
+ run.stored.metrics.pendingGenerationId = run.runId;
4536
4747
  run.stored.messages.push(reservation.userMessage, reservation.assistantMessage);
4537
4748
  if (run.stored.session.agent === undefined) {
4538
4749
  this.pruneReversionState(run.stored, true);
@@ -4650,6 +4861,8 @@ export class FlexHarness<TScope = unknown> {
4650
4861
  prepare: (context: { generationId: string; abortSignal: AbortSignal }) =>
4651
4862
  this.prepareGeneration(run, options, context.abortSignal),
4652
4863
  pendingInput: run.steering.input,
4864
+ activateOnBackgroundJobs: true,
4865
+ onCacheDispatch: (dispatch: plugins.ISmartAiCacheDispatch) => { run.cacheDispatch = dispatch; },
4653
4866
  onInputApplied: (input: plugins.IAgentAppliedInput) => this.applyRunSteer(run, input),
4654
4867
  beforeFinish: (context: { abortSignal: AbortSignal }) =>
4655
4868
  this.awaitNextBackgroundSubagent(run, context.abortSignal),
@@ -4803,7 +5016,8 @@ export class FlexHarness<TScope = unknown> {
4803
5016
  /**
4804
5017
  * Holds the end of a run's generation until one more of its background subagents settles. The
4805
5018
  * subagent commits its result to the generation before it settles, so the model takes the result
4806
- * in with another step instead of the run ending without it. Returns at once when none runs.
5019
+ * in with another step instead of the run ending without it. The engine bounds the scoped
5020
+ * wait from its observed dispatch and commits timer check-ins. Returns at once when none runs.
4807
5021
  */
4808
5022
  private async awaitNextBackgroundSubagent(run: IActiveRun, signal: AbortSignal): Promise<void> {
4809
5023
  if (run.backgroundSubagents.size === 0) return;
@@ -4828,8 +5042,16 @@ export class FlexHarness<TScope = unknown> {
4828
5042
  try {
4829
5043
  // The transactional generation settles only after its model calls are reported, so
4830
5044
  // `run.usage` is complete once it settles, also for a cancelled run.
4831
- const rawResult = await generation;
4832
- generated = true;
5045
+ let rawResult: plugins.IAgentGenerateResult;
5046
+ try {
5047
+ rawResult = await generation;
5048
+ generated = true;
5049
+ } finally {
5050
+ await this.settleMetricsOperation(run.state, run.stored, run.metricsOperation!);
5051
+ }
5052
+ if (rawResult.terminationReason === 'step-limit') {
5053
+ throw this.trustInternalError(new FlexHarnessStepLimitError(rawResult.steps));
5054
+ }
4833
5055
  // Background tools do not block inference, but the owning turn joins its admitted children
4834
5056
  // before releasing its authority or committing its terminal projection.
4835
5057
  await Promise.all([...run.backgroundSubagents]);
@@ -5617,18 +5839,37 @@ export class FlexHarness<TScope = unknown> {
5617
5839
  }
5618
5840
  signal.throwIfAborted();
5619
5841
  run.modelResolution = model!;
5842
+ run.toolProviderContext = toolProviderContext as Readonly<IFlexToolProviderContext<unknown>>;
5843
+ let catalog: FlexToolCatalog<unknown> | undefined;
5844
+ if (toolHandle?.catalog) {
5845
+ try {
5846
+ catalog = await FlexToolCatalog.create(toolHandle.catalog, this.toolCatalogOptions);
5847
+ } catch (error) {
5848
+ throw this.projectExternalError(run, error, 'toolProvider');
5849
+ }
5850
+ signal.throwIfAborted();
5851
+ }
5620
5852
  let tools: TFlexAgentToolSet | undefined;
5621
5853
  try {
5622
5854
  const providedTools = toolHandle?.tools;
5623
5855
  const combinedTools: Record<string, unknown> = { ...(providedTools ?? {}) };
5624
5856
  Object.assign(combinedTools, this.createBuiltInTools(run));
5857
+ if (catalog) assertNoCatalogCollisions(catalog, combinedTools);
5625
5858
  const globalNames = run.allowedTools === undefined ? undefined : new Set(run.allowedTools);
5626
5859
  const agentNames = run.agentAllowedTools === undefined ? undefined : new Set(run.agentAllowedTools);
5860
+ const admitted = (name: string): boolean =>
5861
+ (globalNames === undefined || globalNames.has(name))
5862
+ && (agentNames === undefined || agentNames.has(name));
5627
5863
  const admittedTools = globalNames === undefined && agentNames === undefined
5628
5864
  ? combinedTools
5629
- : Object.fromEntries(Object.entries(combinedTools).filter(([name]) =>
5630
- (globalNames === undefined || globalNames.has(name))
5631
- && (agentNames === undefined || agentNames.has(name))));
5865
+ : Object.fromEntries(Object.entries(combinedTools).filter(([name]) => admitted(name)));
5866
+ const admittedCatalog = catalog && (globalNames === undefined && agentNames === undefined
5867
+ ? catalog
5868
+ : catalog.restrict(catalog.methods.filter(admitted)));
5869
+ if (admittedCatalog && admittedCatalog.methods.length > 0) {
5870
+ run.toolCatalog = admittedCatalog;
5871
+ Object.assign(admittedTools, this.createCatalogTools(run, admittedCatalog));
5872
+ }
5632
5873
  tools = Object.keys(admittedTools).length > 0
5633
5874
  ? wrapToolSet(
5634
5875
  admittedTools as TFlexAgentToolSet,
@@ -5644,6 +5885,9 @@ export class FlexHarness<TScope = unknown> {
5644
5885
  const close = toolHandle?.close
5645
5886
  ? async () => {
5646
5887
  if (closed) return;
5888
+ // Inference has ended before its tool lease closes. Fence follow-up admission
5889
+ // synchronously, even when external cleanup has not settled yet.
5890
+ run.steering.input.close();
5647
5891
  closePromise ??= Promise.resolve()
5648
5892
  .then(() => toolHandle!.close!())
5649
5893
  .then(() => {
@@ -5684,6 +5928,7 @@ export class FlexHarness<TScope = unknown> {
5684
5928
  if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
5685
5929
  names.add('delegate');
5686
5930
  names.add('delegate_result');
5931
+ names.add('delegate_send');
5687
5932
  }
5688
5933
  if (this.builtInTools.renameSession) names.add('rename_session');
5689
5934
  const projectManagement = this.builtInTools.projectManagement;
@@ -5703,11 +5948,65 @@ export class FlexHarness<TScope = unknown> {
5703
5948
  }
5704
5949
  }
5705
5950
 
5951
+ /**
5952
+ * search_tools, describe_tools and execute. `execute` validates the call and runs the method
5953
+ * inside this tool call, so the tool wrapper bounds its output and projects its errors, and the
5954
+ * call's part, events and permission requests carry the method's path.
5955
+ */
5956
+ private createCatalogTools(run: IActiveRun, catalog: FlexToolCatalog<unknown>): Record<string, unknown> {
5957
+ const names = FLEX_TOOL_CATALOG_TOOL_NAMES;
5958
+ const definitions = catalog.toolDefinitions();
5959
+ const describeBytes = Math.max(1024, this.toolOutputLimits.maxBytes - 1024);
5960
+ return {
5961
+ [names.search]: plugins.tool({
5962
+ description: definitions[names.search].description,
5963
+ strict: false,
5964
+ inputSchema: catalogToolInputSchema<IFlexCatalogSearchInput>(definitions[names.search]),
5965
+ execute: async (input: IFlexCatalogSearchInput) => catalog.search(input),
5966
+ }),
5967
+ [names.describe]: plugins.tool({
5968
+ description: definitions[names.describe].description,
5969
+ strict: false,
5970
+ inputSchema: catalogToolInputSchema<IFlexCatalogDescribeInput>(definitions[names.describe]),
5971
+ execute: async (input: IFlexCatalogDescribeInput, options?: { messages?: plugins.ModelMessage[] }) =>
5972
+ catalog.describe(input, {
5973
+ declaredTypes: catalog.declaredTypesIn(options?.messages),
5974
+ maxBytes: describeBytes,
5975
+ }).output,
5976
+ }),
5977
+ [names.execute]: plugins.tool({
5978
+ description: definitions[names.execute].description,
5979
+ strict: false,
5980
+ inputSchema: catalogToolInputSchema<{ method: string; params?: Record<string, unknown> }>(
5981
+ definitions[names.execute],
5982
+ ),
5983
+ execute: async (
5984
+ input: { method: string; params?: Record<string, unknown> },
5985
+ options?: { toolCallId?: string; abortSignal?: AbortSignal },
5986
+ ) => {
5987
+ const call = await catalog.prepareCall(input.method, input.params);
5988
+ if (!call.success) {
5989
+ const error = this.trustInternalError(new FlexHarnessValidationError(JSON.stringify(call.error)));
5990
+ if (options?.toolCallId) run.trustedToolExecutionErrors.set(options.toolCallId, error);
5991
+ throw error;
5992
+ }
5993
+ return call.run({
5994
+ method: call.method,
5995
+ toolCallId: options?.toolCallId ?? '',
5996
+ abortSignal: options?.abortSignal ?? run.controller.signal,
5997
+ provider: run.toolProviderContext!,
5998
+ });
5999
+ },
6000
+ }),
6001
+ };
6002
+ }
6003
+
5706
6004
  private createBuiltInTools(run: IActiveRun): Record<string, unknown> {
5707
6005
  const tools: Record<string, unknown> = {};
5708
6006
  if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
5709
6007
  tools.delegate = this.createSubagentTool(run);
5710
6008
  tools.delegate_result = this.createSubagentResultTool(run);
6009
+ tools.delegate_send = this.createSubagentSendTool(run);
5711
6010
  }
5712
6011
  if (this.builtInTools.renameSession) tools.rename_session = this.createRenameSessionTool(run);
5713
6012
  const projectManagement = this.builtInTools.projectManagement;
@@ -6253,9 +6552,33 @@ export class FlexHarness<TScope = unknown> {
6253
6552
  }
6254
6553
  }
6255
6554
 
6555
+ private createSubagentSendTool(run: IActiveRun): unknown {
6556
+ return plugins.tool({
6557
+ description: 'Send follow-up instructions to a RUNNING direct child owned by this exact parent turn. The child takes the message at its next inference boundary; running tools and permission waits are not interrupted. Never starts or resumes a settled child. Use delegate_result for progress and results.',
6558
+ inputSchema: plugins.z.object({
6559
+ taskId: plugins.z.string().min(1).max(maxSubagentTaskIdBytes),
6560
+ prompt: plugins.z.string().min(1).max(maxSubagentPromptBytes),
6561
+ }).strict(),
6562
+ execute: async (
6563
+ { taskId, prompt }: { taskId: string; prompt: string },
6564
+ options?: { toolCallId?: string },
6565
+ ) => {
6566
+ validateUtf8String(prompt, 'delegate_send prompt', maxSubagentPromptBytes, true);
6567
+ const toolCallId = options?.toolCallId;
6568
+ validateUtf8String(toolCallId, 'delegate_send toolCallId', maxTransferIdentifierBytes, true);
6569
+ return this.sendSubagentInputInRun(run, taskId, {
6570
+ steerId: `steer_${sha256Hex(JSON.stringify([
6571
+ 'flexharness-subagent-input-v1', run.state.storageKey, run.sessionId, run.runId, toolCallId,
6572
+ ]))}`,
6573
+ prompt,
6574
+ });
6575
+ },
6576
+ });
6577
+ }
6578
+
6256
6579
  private createSubagentResultTool(run: IActiveRun): unknown {
6257
6580
  return plugins.tool({
6258
- description: 'Read a subagent owned by this parent. Returns live status and the final text when settled. waitMs optionally waits for completion, bounded to 240 seconds; omit it to continue immediately.',
6581
+ description: 'Read a subagent owned by this parent. Returns live status and the final text when settled. waitMs optionally waits for completion, bounded to 240 seconds and the remaining configured cache idle budget; omit it to continue immediately.',
6259
6582
  inputSchema: plugins.z.object({
6260
6583
  taskId: plugins.z.string().min(1).max(maxSubagentTaskIdBytes),
6261
6584
  waitMs: plugins.z.number().int().min(0).max(240_000).optional(),
@@ -6274,7 +6597,7 @@ export class FlexHarness<TScope = unknown> {
6274
6597
  await Promise.race([
6275
6598
  active.completion.catch(() => undefined),
6276
6599
  new Promise<void>((resolve, reject) => {
6277
- timer = setTimeout(resolve, waitMs);
6600
+ timer = setTimeout(resolve, plugins.getSmartAiCacheWaitMs(waitMs, run.cacheDispatch));
6278
6601
  onAbort = () => reject(run.controller.signal.reason);
6279
6602
  run.controller.signal.addEventListener('abort', onAbort, { once: true });
6280
6603
  if (run.controller.signal.aborted) onAbort();
@@ -6328,6 +6651,9 @@ export class FlexHarness<TScope = unknown> {
6328
6651
  parentQueueId: run.queueId,
6329
6652
  parentRunId: run.runId,
6330
6653
  parentToolCallId: toolCallId,
6654
+ ...(run.stored.session.configurationRef === undefined
6655
+ ? {}
6656
+ : { configurationRef: run.stored.session.configurationRef }),
6331
6657
  agent: definition.name,
6332
6658
  depth: (run.stored.session.depth ?? 0) + 1,
6333
6659
  });
@@ -6467,6 +6793,7 @@ export class FlexHarness<TScope = unknown> {
6467
6793
  run.scopeId,
6468
6794
  run.scope as TScope,
6469
6795
  placeholder!.compactorLifecycleController,
6796
+ true,
6470
6797
  );
6471
6798
  const parentLostOwnership = run.controller.signal.aborted
6472
6799
  || state.sessions.get(run.sessionId) !== run.stored
@@ -8183,13 +8510,17 @@ export class FlexHarness<TScope = unknown> {
8183
8510
  const run = this.callbackRun(state, sessionId);
8184
8511
  if (!run || run.toolPartIds.has(event.toolCallId)) return;
8185
8512
  try {
8186
- const input = normalizeJsonValue(event.input, this.toolOutputLimits);
8513
+ // An execute call is the call of the catalog method it names.
8514
+ const target = event.toolName === FLEX_TOOL_CATALOG_TOOL_NAMES.execute
8515
+ ? run.toolCatalog?.callTarget(event.input)
8516
+ : undefined;
8517
+ const input = normalizeJsonValue(target ? target.params : event.input, this.toolOutputLimits);
8187
8518
  if (!this.reserveCallbackCapacity(run, 1, jsonBytes(input), 1)) return;
8188
8519
  const part: IFlexToolMessagePart = {
8189
8520
  partId: plugins.crypto.randomUUID(),
8190
8521
  type: 'tool',
8191
8522
  toolCallId: this.boundedIdentifier(event.toolCallId, 'toolCallId'),
8192
- toolName: this.boundedIdentifier(event.toolName, 'toolName'),
8523
+ toolName: this.boundedIdentifier(target ? target.method : event.toolName, 'toolName'),
8193
8524
  status: 'running',
8194
8525
  input,
8195
8526
  startedAt: new Date().toISOString(),
@@ -8289,34 +8620,59 @@ export class FlexHarness<TScope = unknown> {
8289
8620
  stored: IStoredSessionState,
8290
8621
  event: plugins.TAgentModelCallUsageEvent,
8291
8622
  ): void {
8292
- const { source, generationId, ...call } = event;
8293
- const sessionId = stored.session.sessionId;
8294
- const activeRun = state.activeRuns.get(sessionId);
8295
- const run = generationId !== undefined && activeRun?.runId === generationId ? activeRun : undefined;
8296
- let scopeId = run?.scopeId;
8297
- if (scopeId === undefined) {
8298
- const ambientContext = this.compactorInvocationContext.getStore();
8299
- const invocationContext = ambientContext
8300
- ? this.deferredCompactorContexts.get(ambientContext) ?? ambientContext
8301
- : undefined;
8302
- if (
8303
- !invocationContext
8304
- || invocationContext.storageKey !== state.storageKey
8305
- || invocationContext.sessionId !== sessionId
8306
- ) {
8307
- throw new Error('Model call usage is missing its exact FlexHarness invocation context.');
8623
+ try {
8624
+ const { source, generationId, ...call } = event;
8625
+ const sessionId = stored.session.sessionId;
8626
+ const activeRun = state.activeRuns.get(sessionId);
8627
+ const run = generationId !== undefined && activeRun?.runId === generationId ? activeRun : undefined;
8628
+ const operation = source === 'compaction' ? stored.metricsCompaction : run?.metricsOperation;
8629
+ if (!operation) throw new Error('Model call usage has no owning metrics operation.');
8630
+ observeSessionMetricsCall(operation, call);
8631
+ let scopeId = run?.scopeId;
8632
+ if (scopeId === undefined) {
8633
+ const ambientContext = this.compactorInvocationContext.getStore();
8634
+ const invocationContext = ambientContext
8635
+ ? this.deferredCompactorContexts.get(ambientContext) ?? ambientContext
8636
+ : undefined;
8637
+ if (
8638
+ !invocationContext
8639
+ || invocationContext.storageKey !== state.storageKey
8640
+ || invocationContext.sessionId !== sessionId
8641
+ ) {
8642
+ throw new Error('Model call usage is missing its exact FlexHarness invocation context.');
8643
+ }
8644
+ scopeId = invocationContext.scopeId;
8645
+ }
8646
+ if (run && call.status === 'reported') {
8647
+ for (const key of Object.keys(run.usage) as (keyof IFlexUsage)[]) run.usage[key] += call.usage[key];
8308
8648
  }
8309
- scopeId = invocationContext.scopeId;
8649
+ this.emitEvent(scopeId, stored.session, {
8650
+ type: 'model.usage',
8651
+ ...(generationId === undefined ? {} : { runId: generationId }),
8652
+ source,
8653
+ ...call,
8654
+ });
8655
+ } catch (error) {
8656
+ // AgentSession isolates the entire callback, including event/context failures. Join it later.
8657
+ stored.metricsFailure ??= error;
8658
+ throw error;
8310
8659
  }
8311
- if (run && call.status === 'reported') {
8312
- for (const key of Object.keys(run.usage) as (keyof IFlexUsage)[]) run.usage[key] += call.usage[key];
8660
+ }
8661
+
8662
+ private async settleMetricsOperation(
8663
+ state: IStorageState,
8664
+ stored: IStoredSessionState,
8665
+ operation: ISessionMetricsOperation,
8666
+ ): Promise<void> {
8667
+ try {
8668
+ if (stored.metricsFailure !== undefined) throw stored.metricsFailure;
8669
+ await this.mutateProjection(state, stored, () => settleSessionMetricsOperation(stored.metrics, operation), true);
8670
+ } catch (error) {
8671
+ stored.metricsFailure ??= error;
8672
+ // Never allow a later operation to overwrite a marker whose spend was not durably joined.
8673
+ state.lifecycle = 'fenced';
8674
+ throw error;
8313
8675
  }
8314
- this.emitEvent(scopeId, stored.session, {
8315
- type: 'model.usage',
8316
- ...(generationId === undefined ? {} : { runId: generationId }),
8317
- source,
8318
- ...call,
8319
- });
8320
8676
  }
8321
8677
 
8322
8678
  private callbackRun(state: IStorageState, sessionId: string): IActiveRun | undefined {
@@ -8820,9 +9176,12 @@ export class FlexHarness<TScope = unknown> {
8820
9176
  errors.push(this.projectExternalError(run, error, 'persistence'));
8821
9177
  }
8822
9178
  }
8823
- if (projectionReserved) {
9179
+ if (projectionReserved || run.stored.metrics.pendingGenerationId === run.runId) {
8824
9180
  try {
8825
9181
  await this.mutateProjection(run.state, run.stored, () => {
9182
+ if (run.stored.metrics.pendingGenerationId === run.runId) {
9183
+ delete run.stored.metrics.pendingGenerationId;
9184
+ }
8826
9185
  run.stored.messages = run.stored.messages.filter((message) => message.runId !== run.runId);
8827
9186
  run.stored.stagedTerminals = run.stored.stagedTerminals.filter((entry) => entry.runId !== run.runId);
8828
9187
  run.stored.reversionSegments = run.stored.reversionSegments.filter(
@@ -8862,9 +9221,14 @@ export class FlexHarness<TScope = unknown> {
8862
9221
  }
8863
9222
  const tools = handle.tools;
8864
9223
  this.assertNoBuiltInToolCollisions(run, tools);
9224
+ const catalog = handle.catalog;
9225
+ if (catalog !== undefined && (typeof catalog !== 'object' || catalog === null)) {
9226
+ throw new FlexHarnessValidationError('Tool handle catalog must be an object.');
9227
+ }
8865
9228
  const closeHandle = close;
8866
9229
  return {
8867
9230
  tools,
9231
+ ...(catalog === undefined ? {} : { catalog }),
8868
9232
  ...(closeHandle ? { close: () => closeHandle.call(handle) } : {}),
8869
9233
  };
8870
9234
  } catch (error) {
@@ -8951,6 +9315,9 @@ export class FlexHarness<TScope = unknown> {
8951
9315
  );
8952
9316
  if (!handle) continue;
8953
9317
  owned.push({ handle, closed: false });
9318
+ if (handle.catalog !== undefined) {
9319
+ throw new FlexHarnessValidationError('Resource tool providers cannot supply a tool catalog.');
9320
+ }
8954
9321
  for (const [toolName, tool] of Object.entries(handle.tools)) {
8955
9322
  const exposedName = createFlexResourceToolName(namespace, toolName);
8956
9323
  if (Object.prototype.hasOwnProperty.call(tools, exposedName)) {
@@ -8962,6 +9329,7 @@ export class FlexHarness<TScope = unknown> {
8962
9329
  if (owned.length === 0 && Object.keys(tools).length === 0) return undefined;
8963
9330
  return {
8964
9331
  tools: tools as TFlexAgentToolSet,
9332
+ ...(applicationHandle?.catalog === undefined ? {} : { catalog: applicationHandle.catalog }),
8965
9333
  ...(owned.some((entry) => entry.handle.close) ? { close: closeOwned } : {}),
8966
9334
  };
8967
9335
  } catch (error) {
@@ -9403,6 +9771,7 @@ export class FlexHarness<TScope = unknown> {
9403
9771
  projectionReconciliationRequired: false,
9404
9772
  projectionRevision: 0,
9405
9773
  projectionQueue: Promise.resolve(),
9774
+ metrics: createSessionMetricsState(false),
9406
9775
  rememberedPermissionKeys: new Set(),
9407
9776
  permissionRevision: 0,
9408
9777
  permissionQueue: Promise.resolve(),
@@ -9427,6 +9796,7 @@ export class FlexHarness<TScope = unknown> {
9427
9796
  scopeId: string,
9428
9797
  scope: TScope,
9429
9798
  compactorLifecycleController = new AbortController(),
9799
+ newSession = false,
9430
9800
  ): Promise<IStoredSessionState> {
9431
9801
  const sessionId = metadata.sessionId;
9432
9802
  const compactorContext = this.createCompactorContext(
@@ -9530,11 +9900,12 @@ export class FlexHarness<TScope = unknown> {
9530
9900
  pendingReversionReleases,
9531
9901
  projectionSchemaVersion: projection?.schemaVersion ?? 3,
9532
9902
  projectionReconciliationRequired: false,
9533
- ...(projection && projection.schemaVersion !== 3
9903
+ ...(projection
9534
9904
  ? { projectionBaseline: cloneSerializable(projection) }
9535
9905
  : {}),
9536
9906
  projectionRevision: projection?.revision ?? 0,
9537
9907
  projectionQueue: Promise.resolve(),
9908
+ metrics: cloneSerializable(currentProjection?.metrics ?? createSessionMetricsState(newSession)),
9538
9909
  rememberedPermissionKeys: new Set(permission?.rememberedPermissionKeys ?? []),
9539
9910
  permissionRevision: permission?.revision ?? 0,
9540
9911
  permissionQueue: Promise.resolve(),
@@ -9553,6 +9924,19 @@ export class FlexHarness<TScope = unknown> {
9553
9924
  terminalPromptQueueEntries: new Map(),
9554
9925
  outstandingPromptBytes: 0,
9555
9926
  };
9927
+ if (currentProjection?.metrics === undefined
9928
+ || stored.metrics.pendingGenerationId !== undefined
9929
+ || stored.metrics.pendingCompactionId !== undefined
9930
+ || (metadata.origin !== undefined && stored.metrics.lifetimeUsageComplete)) {
9931
+ await this.mutateProjection(state, stored, () => {
9932
+ if (stored.metrics.pendingGenerationId !== undefined || stored.metrics.pendingCompactionId !== undefined
9933
+ || metadata.origin !== undefined) {
9934
+ stored.metrics.lifetimeUsageComplete = false;
9935
+ }
9936
+ delete stored.metrics.pendingGenerationId;
9937
+ delete stored.metrics.pendingCompactionId;
9938
+ });
9939
+ }
9556
9940
  const {
9557
9941
  contextCompactor,
9558
9942
  ...agentSessionPolicy
@@ -9609,22 +9993,55 @@ export class FlexHarness<TScope = unknown> {
9609
9993
  events: filteredEvents,
9610
9994
  }),
9611
9995
  );
9612
- return contextCompactor(filteredMessages, filteredEvents, {
9613
- ...options,
9614
- ...invocation,
9615
- scope: invocation.scope as TScope,
9616
- ...this.compactionModel(state, sessionId, ambientContext, options.reason, modelHint),
9617
- abortSignal: options.abortSignal
9618
- ? AbortSignal.any([
9619
- options.abortSignal,
9620
- state.compactorLifecycleController.signal,
9621
- stored.compactorLifecycleController.signal,
9622
- ])
9623
- : AbortSignal.any([
9624
- state.compactorLifecycleController.signal,
9625
- stored.compactorLifecycleController.signal,
9626
- ]),
9627
- });
9996
+ const operation = createSessionMetricsOperation(plugins.crypto.randomUUID(), 'compaction');
9997
+ try {
9998
+ await this.mutateProjection(state, stored, () => {
9999
+ if (stored.metrics.pendingCompactionId !== undefined || stored.metricsFailure !== undefined) {
10000
+ throw new Error('Session metrics compaction ownership is unresolved.');
10001
+ }
10002
+ stored.metrics.pendingCompactionId = operation.id;
10003
+ });
10004
+ } catch (error) {
10005
+ // No compactor ran. Reconcile a known committed reservation without inventing spend.
10006
+ // Unknown outcomes remain fenced and are repaired conservatively on namespace reload.
10007
+ if (state.lifecycle !== 'fenced' && stored.metrics.pendingCompactionId === operation.id) {
10008
+ try {
10009
+ await this.mutateProjection(state, stored, () => {
10010
+ delete stored.metrics.pendingCompactionId;
10011
+ }, true);
10012
+ } catch (cleanupError) {
10013
+ stored.metricsFailure ??= cleanupError;
10014
+ state.lifecycle = 'fenced';
10015
+ throw combineErrors([error, cleanupError]);
10016
+ }
10017
+ }
10018
+ throw error;
10019
+ }
10020
+ stored.metricsCompaction = operation;
10021
+ try {
10022
+ return await contextCompactor(filteredMessages, filteredEvents, {
10023
+ ...options,
10024
+ ...invocation,
10025
+ scope: invocation.scope as TScope,
10026
+ ...this.compactionModel(state, sessionId, ambientContext, options.reason, modelHint),
10027
+ abortSignal: options.abortSignal
10028
+ ? AbortSignal.any([
10029
+ options.abortSignal,
10030
+ state.compactorLifecycleController.signal,
10031
+ stored.compactorLifecycleController.signal,
10032
+ ])
10033
+ : AbortSignal.any([
10034
+ state.compactorLifecycleController.signal,
10035
+ stored.compactorLifecycleController.signal,
10036
+ ]),
10037
+ });
10038
+ } finally {
10039
+ try {
10040
+ await this.settleMetricsOperation(state, stored, operation);
10041
+ } finally {
10042
+ delete stored.metricsCompaction;
10043
+ }
10044
+ }
9628
10045
  },
9629
10046
  }
9630
10047
  : {}),
@@ -10044,6 +10461,7 @@ export class FlexHarness<TScope = unknown> {
10044
10461
  const beforeBaseline = stored.projectionBaseline === undefined
10045
10462
  ? undefined
10046
10463
  : cloneSerializable(stored.projectionBaseline);
10464
+ const beforeMetrics = cloneSerializable(stored.metrics);
10047
10465
  const beforeMessages = cloneSerializable(stored.messages);
10048
10466
  const beforeStages = cloneSerializable(stored.stagedTerminals);
10049
10467
  const beforeSegments = cloneSerializable(stored.reversionSegments);
@@ -10056,6 +10474,7 @@ export class FlexHarness<TScope = unknown> {
10056
10474
  const beforeSnapshot: TFlexProjectionSnapshot = beforeBaseline ?? {
10057
10475
  schemaVersion: 3,
10058
10476
  revision: beforeRevision,
10477
+ metrics: cloneSerializable(beforeMetrics),
10059
10478
  messages: cloneSerializable(beforeMessages),
10060
10479
  stagedTerminals: cloneSerializable(beforeStages),
10061
10480
  reversionSegments: cloneSerializable(beforeSegments),
@@ -10072,6 +10491,7 @@ export class FlexHarness<TScope = unknown> {
10072
10491
  if (beforeBaseline === undefined) delete stored.projectionBaseline;
10073
10492
  else stored.projectionBaseline = beforeBaseline;
10074
10493
  stored.projectionRevision = beforeRevision;
10494
+ stored.metrics = beforeMetrics;
10075
10495
  stored.messages = beforeMessages;
10076
10496
  stored.stagedTerminals = beforeStages;
10077
10497
  stored.reversionSegments = beforeSegments;
@@ -10251,6 +10671,7 @@ export class FlexHarness<TScope = unknown> {
10251
10671
  return {
10252
10672
  schemaVersion: 3,
10253
10673
  revision,
10674
+ metrics: cloneSerializable(stored.metrics),
10254
10675
  messages: cloneSerializable(stored.messages),
10255
10676
  stagedTerminals: cloneSerializable(stored.stagedTerminals),
10256
10677
  reversionSegments: cloneSerializable(stored.reversionSegments),
@@ -10962,6 +11383,11 @@ export class FlexHarness<TScope = unknown> {
10962
11383
  errors.push(error);
10963
11384
  }
10964
11385
  }
11386
+ if (stored.metricsFailure !== undefined) {
11387
+ errors.push(stored.metricsFailure);
11388
+ // Runtime ownership has settled; the fenced namespace will be reloaded from its durable WAL.
11389
+ if (stored.agentSessionCloseCompleted) delete stored.metricsFailure;
11390
+ }
10965
11391
  if (errors.length > 0) throw combineErrors(errors);
10966
11392
  }
10967
11393