@modelprofile.com/flexharness 10.10.0 → 10.12.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,
@@ -95,6 +96,8 @@ import type {
95
96
  IFlexSessionPermissionPolicyContext,
96
97
  IFlexSessionActivity,
97
98
  IFlexSessionGeneration,
99
+ IFlexSessionMetrics,
100
+ IFlexSessionMetricsState,
98
101
  IFlexSessionGenerationCohortEntry,
99
102
  IFlexSessionImportHandle,
100
103
  IFlexSessionImportLimits,
@@ -226,6 +229,15 @@ import {
226
229
  validateUtf8String,
227
230
  } from './utils.validation.js';
228
231
 
232
+ import {
233
+ createSessionMetricsOperation,
234
+ createSessionMetricsState,
235
+ observeSessionMetricsCall,
236
+ sessionMetricsView,
237
+ settleSessionMetricsOperation,
238
+ type ISessionMetricsOperation,
239
+ } from './utils.sessionmetrics.js';
240
+
229
241
  type TCanonicalOutcome = 'accepted' | 'rejected' | 'interrupted';
230
242
  type TRunPhase =
231
243
  | 'admitting'
@@ -256,6 +268,10 @@ interface IStoredSessionState {
256
268
  projectionReconciliationRequired: boolean;
257
269
  projectionRevision: number;
258
270
  projectionQueue: Promise<void>;
271
+ metrics: IFlexSessionMetricsState;
272
+ metricsCompaction?: ISessionMetricsOperation;
273
+ /** Retained independently of the error-neutral projectionQueue. */
274
+ metricsFailure?: unknown;
259
275
  rememberedPermissionKeys: Set<string>;
260
276
  permissionRevision: number;
261
277
  permissionQueue: Promise<void>;
@@ -402,12 +418,14 @@ interface IActiveRun {
402
418
  finishedEventEmitted: boolean;
403
419
  /** The usage the provider reported for the run's model calls so far. */
404
420
  usage: IFlexUsage;
421
+ metricsOperation?: ISessionMetricsOperation;
405
422
  reasoningPartIds: Map<string, string>;
406
423
  toolPartIds: Map<string, string>;
407
424
  trustedToolExecutionErrors: Map<string, Error>;
408
425
  subagentCallCount: number;
409
426
  subagentSessionIds: Set<string>;
410
427
  backgroundSubagents: Set<Promise<void>>;
428
+ cacheDispatch?: plugins.ISmartAiCacheDispatch;
411
429
  pendingPermissionIds: Set<string>;
412
430
  phase: TRunPhase;
413
431
  reservedUserMessage?: IFlexMessage;
@@ -466,6 +484,7 @@ interface IDelegatedRunAdmissionSeed {
466
484
  readonly parentQueueId: string;
467
485
  readonly parentRunId: string;
468
486
  readonly parentToolCallId: string;
487
+ readonly configurationRef?: string;
469
488
  readonly agent: string;
470
489
  readonly depth: number;
471
490
  }
@@ -672,7 +691,7 @@ const maxSubagentPromptBytes = 64 * 1024;
672
691
  const maxSubagentTaskIdBytes = 512;
673
692
  const maxSubagentPermissionMetadataBytes = 16 * 1024;
674
693
  const maxSubagentResultTextBytes = 64 * 1024;
675
- const defaultMaxSubagentDepth = 1;
694
+ const defaultMaxSubagentDepth = 6;
676
695
  const maximumMaxSubagentDepth = 8;
677
696
  const defaultMaxSubagentCallsPerRun = 32;
678
697
  const maximumMaxSubagentCallsPerRun = 128;
@@ -1029,6 +1048,7 @@ export class FlexHarness<TScope = unknown> {
1029
1048
  scopeId,
1030
1049
  resolved.scope.scope,
1031
1050
  placeholder.compactorLifecycleController,
1051
+ true,
1032
1052
  );
1033
1053
  const loaded = await initialization;
1034
1054
  if (resolved.state.sessions.get(sessionId) !== placeholder) {
@@ -1314,6 +1334,7 @@ export class FlexHarness<TScope = unknown> {
1314
1334
  await this.stores.projections.save(state.storageKey, sessionId, {
1315
1335
  schemaVersion: 3,
1316
1336
  revision: 1,
1337
+ metrics: createSessionMetricsState(false),
1317
1338
  messages: staged.runs.flatMap((run) => [run.user, run.assistant]),
1318
1339
  stagedTerminals: [],
1319
1340
  reversionSegments: [],
@@ -1390,6 +1411,16 @@ export class FlexHarness<TScope = unknown> {
1390
1411
  return publicSnapshot(this.requireSession(state, sessionId).session);
1391
1412
  }
1392
1413
 
1414
+ /** Bounded projection-state read: no message, event, archive or child-history scan. */
1415
+ public async getSessionMetrics(scopeId: string, sessionId: string): Promise<IFlexSessionMetrics> {
1416
+ const { state } = await this.resolveState(scopeId);
1417
+ const stored = this.requireSession(state, sessionId);
1418
+ await stored.projectionQueue;
1419
+ this.assertStateAcceptingWork(state);
1420
+ if (stored.metricsFailure !== undefined) throw stored.metricsFailure;
1421
+ return publicSnapshot(sessionMetricsView(stored.metrics));
1422
+ }
1423
+
1393
1424
  public async getSessionReversionInfo(
1394
1425
  scopeId: string,
1395
1426
  sessionId: string,
@@ -3014,13 +3045,53 @@ export class FlexHarness<TScope = unknown> {
3014
3045
  }
3015
3046
  throw new FlexHarnessNotFoundError('Prompt queue entry', queueId);
3016
3047
  }
3048
+ return this.admitRunSteer(run, steer, prompt, byteSize);
3049
+ }
3050
+
3051
+ /** Sends follow-up input only to an exact running direct child owned by this parent run. */
3052
+ public async sendSubagentInput(
3053
+ scopeId: string,
3054
+ parentSessionId: string,
3055
+ parentRunId: string,
3056
+ taskId: string,
3057
+ steer: IFlexSteerInput,
3058
+ ): Promise<IFlexSteerAdmission> {
3059
+ validateIdentifier(parentSessionId, 'parentSessionId');
3060
+ requireTransferIdentifier(parentSessionId, 'parentSessionId');
3061
+ validateIdentifier(parentRunId, 'parentRunId');
3062
+ requireTransferIdentifier(parentRunId, 'parentRunId');
3063
+ this.assertOpen();
3064
+ const { state } = await this.resolveState(scopeId);
3065
+ const parent = state.activeRuns.get(parentSessionId);
3066
+ if (!parent || parent.runId !== parentRunId) {
3067
+ throw new FlexHarnessValidationError('The exact parent run is not active.');
3068
+ }
3069
+ return this.sendSubagentInputInRun(parent, taskId, steer);
3070
+ }
3071
+
3072
+ /** The shared synchronous admission point; neither entrypoint starts or resumes a run. */
3073
+ private admitRunSteer(
3074
+ run: IActiveRun,
3075
+ steer: IFlexSteerInput,
3076
+ prompt: INormalizedFlexPrompt,
3077
+ byteSize: number,
3078
+ ): IFlexSteerAdmission {
3079
+ if (
3080
+ run.phase === 'closing'
3081
+ || run.phase === 'finalizing'
3082
+ || run.phase === 'promoting'
3083
+ || run.callbacksClosed
3084
+ || run.controller.signal.aborted
3085
+ || run.stop.signal.aborted
3086
+ || run.state.activeRuns.get(run.sessionId) !== run
3087
+ ) throw new FlexHarnessSteerRejectedError('not-running', steer.steerId);
3017
3088
  const steering = run.steering;
3018
3089
  if (!steering.input.closed && !steering.accepted.has(steer.steerId)) {
3019
3090
  if (
3020
3091
  steering.accepted.size - steering.applied.size
3021
3092
  >= this.promptQueueLimits.maxOutstandingPromptsPerSession
3022
3093
  ) {
3023
- throw new FlexHarnessQueueFullError(`Session "${sessionId}" has reached its pending steer limit.`);
3094
+ throw new FlexHarnessQueueFullError(`Session "${run.sessionId}" has reached its pending steer limit.`);
3024
3095
  }
3025
3096
  // Pending steers hold outstanding prompt bytes of the session until they are taken in.
3026
3097
  if (
@@ -3028,7 +3099,7 @@ export class FlexHarness<TScope = unknown> {
3028
3099
  > this.promptQueueLimits.maxOutstandingBytesPerSession
3029
3100
  ) {
3030
3101
  throw new FlexHarnessQueueFullError(
3031
- `Session "${sessionId}" has reached its outstanding prompt byte limit.`,
3102
+ `Session "${run.sessionId}" has reached its outstanding prompt byte limit.`,
3032
3103
  );
3033
3104
  }
3034
3105
  }
@@ -3052,11 +3123,130 @@ export class FlexHarness<TScope = unknown> {
3052
3123
  steering.stored.outstandingPromptBytes += byteSize;
3053
3124
  this.emitEvent(run.scopeId, run.stored.session, {
3054
3125
  type: 'prompt.steer.accepted',
3055
- queueId,
3126
+ queueId: run.queueId,
3056
3127
  runId: run.runId,
3057
3128
  steerId: steer.steerId,
3058
3129
  });
3059
- return { steerId: steer.steerId, queueId, runId: run.runId };
3130
+ return { steerId: steer.steerId, queueId: run.queueId, runId: run.runId };
3131
+ }
3132
+
3133
+ private sendSubagentInputInRun(
3134
+ parent: IActiveRun,
3135
+ taskId: string,
3136
+ steer: IFlexSteerInput,
3137
+ ): IFlexSteerAdmission {
3138
+ this.assertOpen();
3139
+ validateUtf8String(taskId, 'taskId', maxSubagentTaskIdBytes, true);
3140
+ validateIdentifier(steer.steerId, 'steerId');
3141
+ requireTransferIdentifier(steer.steerId, 'steerId');
3142
+ const prompt = normalizeFlexPrompt(steer.prompt);
3143
+ const byteSize = this.promptAdmissionByteSize(steer.prompt, {});
3144
+ this.assertStateAcceptingWork(parent.state);
3145
+ const child = parent.state.activeRuns.get(taskId);
3146
+ if (!child) throw new FlexHarnessSteerRejectedError('not-running', steer.steerId);
3147
+ this.assertLiveSubagentOwnership(child, parent);
3148
+ return this.admitRunSteer(child, steer, prompt, byteSize);
3149
+ }
3150
+
3151
+ /** Fences live ownership by exact runtime pointers and admission seeds all the way to root. */
3152
+ private assertLiveSubagentOwnership(child: IActiveRun, parent: IActiveRun): void {
3153
+ const state = parent.state;
3154
+ const visited = new Set<IActiveRun>();
3155
+ let current = child;
3156
+ let expectedParent: IActiveRun | undefined = parent;
3157
+ while (true) {
3158
+ const stored = current.stored;
3159
+ const session = stored.session;
3160
+ const queued = stored.outstandingPromptsById.get(current.queueId);
3161
+ if (
3162
+ visited.has(current)
3163
+ || current.state !== state
3164
+ || state.activeRuns.get(current.sessionId) !== current
3165
+ || state.sessions.get(current.sessionId) !== stored
3166
+ || session.sessionId !== current.sessionId
3167
+ || state.tombstones.has(current.sessionId)
3168
+ || state.initializingSessions.has(current.sessionId)
3169
+ || current.phase !== 'running'
3170
+ || current.callbacksClosed
3171
+ || current.controller.signal.aborted
3172
+ || current.stop.signal.aborted
3173
+ || current.steering.input.closed
3174
+ || !queued
3175
+ || queued.state !== state
3176
+ || queued.stored !== stored
3177
+ || queued.sessionId !== current.sessionId
3178
+ || queued.queueId !== current.queueId
3179
+ || queued.runId !== current.runId
3180
+ || queued.steering !== current.steering
3181
+ ) throw new FlexHarnessValidationError('The subagent or ancestor run is no longer live and owned.');
3182
+ this.assertSessionWritable(stored);
3183
+ visited.add(current);
3184
+ if (session.parentSessionId === undefined) {
3185
+ if (expectedParent !== undefined || queued.delegationSeed !== undefined) {
3186
+ throw new FlexHarnessValidationError('The task is not a direct child of this parent run.');
3187
+ }
3188
+ return;
3189
+ }
3190
+ const seed = queued.delegationSeed;
3191
+ if (!seed) throw new FlexHarnessValidationError('The child has no live delegated admission owner.');
3192
+ const owner = current.delegatedRunAdmissionOwner;
3193
+ const owningParent = seed.parentRun;
3194
+ const parentSession = owningParent.stored.session;
3195
+ if (
3196
+ (expectedParent !== undefined && owningParent !== expectedParent)
3197
+ || seed.state !== state
3198
+ || seed.storageKey !== state.storageKey
3199
+ || seed.childStored !== stored
3200
+ || stored.storageKey !== seed.storageKey
3201
+ || seed.sessionId !== current.sessionId
3202
+ || session.sessionId !== seed.sessionId
3203
+ || session.sessionGenerationId !== seed.sessionGenerationId
3204
+ || session.sessionGenerationSequence !== seed.sessionGenerationSequence
3205
+ || session.parentSessionId !== seed.parentSessionId
3206
+ || session.parentRunId !== seed.originParentRunId
3207
+ || session.parentToolCallId !== seed.originParentToolCallId
3208
+ || session.agent !== seed.agent
3209
+ || session.depth !== seed.depth
3210
+ || session.configurationRef !== seed.configurationRef
3211
+ || parentSession.configurationRef !== seed.configurationRef
3212
+ || seed.depth !== (parentSession.depth ?? 0) + 1
3213
+ || !owningParent.subagents.has(seed.agent)
3214
+ || seed.parentStored !== owningParent.stored
3215
+ || seed.parentStored.storageKey !== seed.storageKey
3216
+ || owningParent.sessionId !== seed.parentSessionId
3217
+ || owningParent.runId !== seed.parentRunId
3218
+ || owningParent.queueId !== seed.parentQueueId
3219
+ || parentSession.sessionGenerationId !== seed.parentSessionGenerationId
3220
+ || parentSession.sessionGenerationSequence !== seed.parentSessionGenerationSequence
3221
+ || current.scopeId !== seed.scopeId
3222
+ || current.scope !== seed.scope
3223
+ || owningParent.scopeId !== seed.scopeId
3224
+ || owningParent.scope !== seed.scope
3225
+ || (this.delegatedRunAdmissionProvider !== undefined && owner === undefined)
3226
+ || (owner !== undefined && (
3227
+ owner.storageState !== state || owner.run !== current
3228
+ || !state.delegatedRunAdmissionOwners.has(owner)
3229
+ || owner.status !== 'acquired' || !owner.acquisitionSettled || owner.acquisitionDetached
3230
+ || owner.context.runId !== current.runId || owner.context.queueId !== current.queueId
3231
+ || owner.context.parentRunId !== owningParent.runId
3232
+ || owner.context.parentQueueId !== owningParent.queueId
3233
+ || owner.context.scopeId !== seed.scopeId || owner.context.scope !== seed.scope
3234
+ || owner.context.storageKey !== seed.storageKey
3235
+ || owner.context.sessionId !== seed.sessionId
3236
+ || owner.context.parentSessionId !== seed.parentSessionId
3237
+ || owner.context.parentToolCallId !== seed.parentToolCallId
3238
+ || owner.context.originParentRunId !== seed.originParentRunId
3239
+ || owner.context.originParentToolCallId !== seed.originParentToolCallId
3240
+ || owner.context.agent !== seed.agent || owner.context.depth !== seed.depth
3241
+ || owner.context.sessionGenerationId !== seed.sessionGenerationId
3242
+ || owner.context.sessionGenerationSequence !== seed.sessionGenerationSequence
3243
+ || owner.context.parentSessionGenerationId !== seed.parentSessionGenerationId
3244
+ || owner.context.parentSessionGenerationSequence !== seed.parentSessionGenerationSequence
3245
+ ))
3246
+ ) throw new FlexHarnessValidationError('The task is not owned by this exact parent run and configuration.');
3247
+ current = owningParent;
3248
+ expectedParent = undefined;
3249
+ }
3060
3250
  }
3061
3251
 
3062
3252
  public async abort(scopeId: string, sessionId: string): Promise<boolean> {
@@ -4532,7 +4722,12 @@ export class FlexHarness<TScope = unknown> {
4532
4722
  this.assertPromptPromotion(queued, run);
4533
4723
  const reservation = this.createReservation(run, prompt);
4534
4724
  closeReservationPublication = this.openPublication(run.state, run.sessionId);
4725
+ run.metricsOperation = createSessionMetricsOperation(run.runId, 'generation');
4535
4726
  await this.mutateProjection(run.state, run.stored, () => {
4727
+ if (run.stored.metrics.pendingGenerationId !== undefined) {
4728
+ throw new Error('Session metrics already owns a generation.');
4729
+ }
4730
+ run.stored.metrics.pendingGenerationId = run.runId;
4536
4731
  run.stored.messages.push(reservation.userMessage, reservation.assistantMessage);
4537
4732
  if (run.stored.session.agent === undefined) {
4538
4733
  this.pruneReversionState(run.stored, true);
@@ -4650,6 +4845,8 @@ export class FlexHarness<TScope = unknown> {
4650
4845
  prepare: (context: { generationId: string; abortSignal: AbortSignal }) =>
4651
4846
  this.prepareGeneration(run, options, context.abortSignal),
4652
4847
  pendingInput: run.steering.input,
4848
+ activateOnBackgroundJobs: true,
4849
+ onCacheDispatch: (dispatch: plugins.ISmartAiCacheDispatch) => { run.cacheDispatch = dispatch; },
4653
4850
  onInputApplied: (input: plugins.IAgentAppliedInput) => this.applyRunSteer(run, input),
4654
4851
  beforeFinish: (context: { abortSignal: AbortSignal }) =>
4655
4852
  this.awaitNextBackgroundSubagent(run, context.abortSignal),
@@ -4803,7 +5000,8 @@ export class FlexHarness<TScope = unknown> {
4803
5000
  /**
4804
5001
  * Holds the end of a run's generation until one more of its background subagents settles. The
4805
5002
  * 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.
5003
+ * in with another step instead of the run ending without it. The engine bounds the scoped
5004
+ * wait from its observed dispatch and commits timer check-ins. Returns at once when none runs.
4807
5005
  */
4808
5006
  private async awaitNextBackgroundSubagent(run: IActiveRun, signal: AbortSignal): Promise<void> {
4809
5007
  if (run.backgroundSubagents.size === 0) return;
@@ -4828,8 +5026,16 @@ export class FlexHarness<TScope = unknown> {
4828
5026
  try {
4829
5027
  // The transactional generation settles only after its model calls are reported, so
4830
5028
  // `run.usage` is complete once it settles, also for a cancelled run.
4831
- const rawResult = await generation;
4832
- generated = true;
5029
+ let rawResult: plugins.IAgentGenerateResult;
5030
+ try {
5031
+ rawResult = await generation;
5032
+ generated = true;
5033
+ } finally {
5034
+ await this.settleMetricsOperation(run.state, run.stored, run.metricsOperation!);
5035
+ }
5036
+ if (rawResult.terminationReason === 'step-limit') {
5037
+ throw this.trustInternalError(new FlexHarnessStepLimitError(rawResult.steps));
5038
+ }
4833
5039
  // Background tools do not block inference, but the owning turn joins its admitted children
4834
5040
  // before releasing its authority or committing its terminal projection.
4835
5041
  await Promise.all([...run.backgroundSubagents]);
@@ -5644,6 +5850,9 @@ export class FlexHarness<TScope = unknown> {
5644
5850
  const close = toolHandle?.close
5645
5851
  ? async () => {
5646
5852
  if (closed) return;
5853
+ // Inference has ended before its tool lease closes. Fence follow-up admission
5854
+ // synchronously, even when external cleanup has not settled yet.
5855
+ run.steering.input.close();
5647
5856
  closePromise ??= Promise.resolve()
5648
5857
  .then(() => toolHandle!.close!())
5649
5858
  .then(() => {
@@ -5684,6 +5893,7 @@ export class FlexHarness<TScope = unknown> {
5684
5893
  if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
5685
5894
  names.add('delegate');
5686
5895
  names.add('delegate_result');
5896
+ names.add('delegate_send');
5687
5897
  }
5688
5898
  if (this.builtInTools.renameSession) names.add('rename_session');
5689
5899
  const projectManagement = this.builtInTools.projectManagement;
@@ -5708,6 +5918,7 @@ export class FlexHarness<TScope = unknown> {
5708
5918
  if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
5709
5919
  tools.delegate = this.createSubagentTool(run);
5710
5920
  tools.delegate_result = this.createSubagentResultTool(run);
5921
+ tools.delegate_send = this.createSubagentSendTool(run);
5711
5922
  }
5712
5923
  if (this.builtInTools.renameSession) tools.rename_session = this.createRenameSessionTool(run);
5713
5924
  const projectManagement = this.builtInTools.projectManagement;
@@ -6253,9 +6464,33 @@ export class FlexHarness<TScope = unknown> {
6253
6464
  }
6254
6465
  }
6255
6466
 
6467
+ private createSubagentSendTool(run: IActiveRun): unknown {
6468
+ return plugins.tool({
6469
+ 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.',
6470
+ inputSchema: plugins.z.object({
6471
+ taskId: plugins.z.string().min(1).max(maxSubagentTaskIdBytes),
6472
+ prompt: plugins.z.string().min(1).max(maxSubagentPromptBytes),
6473
+ }).strict(),
6474
+ execute: async (
6475
+ { taskId, prompt }: { taskId: string; prompt: string },
6476
+ options?: { toolCallId?: string },
6477
+ ) => {
6478
+ validateUtf8String(prompt, 'delegate_send prompt', maxSubagentPromptBytes, true);
6479
+ const toolCallId = options?.toolCallId;
6480
+ validateUtf8String(toolCallId, 'delegate_send toolCallId', maxTransferIdentifierBytes, true);
6481
+ return this.sendSubagentInputInRun(run, taskId, {
6482
+ steerId: `steer_${sha256Hex(JSON.stringify([
6483
+ 'flexharness-subagent-input-v1', run.state.storageKey, run.sessionId, run.runId, toolCallId,
6484
+ ]))}`,
6485
+ prompt,
6486
+ });
6487
+ },
6488
+ });
6489
+ }
6490
+
6256
6491
  private createSubagentResultTool(run: IActiveRun): unknown {
6257
6492
  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.',
6493
+ 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
6494
  inputSchema: plugins.z.object({
6260
6495
  taskId: plugins.z.string().min(1).max(maxSubagentTaskIdBytes),
6261
6496
  waitMs: plugins.z.number().int().min(0).max(240_000).optional(),
@@ -6274,7 +6509,7 @@ export class FlexHarness<TScope = unknown> {
6274
6509
  await Promise.race([
6275
6510
  active.completion.catch(() => undefined),
6276
6511
  new Promise<void>((resolve, reject) => {
6277
- timer = setTimeout(resolve, waitMs);
6512
+ timer = setTimeout(resolve, plugins.getSmartAiCacheWaitMs(waitMs, run.cacheDispatch));
6278
6513
  onAbort = () => reject(run.controller.signal.reason);
6279
6514
  run.controller.signal.addEventListener('abort', onAbort, { once: true });
6280
6515
  if (run.controller.signal.aborted) onAbort();
@@ -6328,6 +6563,9 @@ export class FlexHarness<TScope = unknown> {
6328
6563
  parentQueueId: run.queueId,
6329
6564
  parentRunId: run.runId,
6330
6565
  parentToolCallId: toolCallId,
6566
+ ...(run.stored.session.configurationRef === undefined
6567
+ ? {}
6568
+ : { configurationRef: run.stored.session.configurationRef }),
6331
6569
  agent: definition.name,
6332
6570
  depth: (run.stored.session.depth ?? 0) + 1,
6333
6571
  });
@@ -6467,6 +6705,7 @@ export class FlexHarness<TScope = unknown> {
6467
6705
  run.scopeId,
6468
6706
  run.scope as TScope,
6469
6707
  placeholder!.compactorLifecycleController,
6708
+ true,
6470
6709
  );
6471
6710
  const parentLostOwnership = run.controller.signal.aborted
6472
6711
  || state.sessions.get(run.sessionId) !== run.stored
@@ -8289,34 +8528,59 @@ export class FlexHarness<TScope = unknown> {
8289
8528
  stored: IStoredSessionState,
8290
8529
  event: plugins.TAgentModelCallUsageEvent,
8291
8530
  ): 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.');
8531
+ try {
8532
+ const { source, generationId, ...call } = event;
8533
+ const sessionId = stored.session.sessionId;
8534
+ const activeRun = state.activeRuns.get(sessionId);
8535
+ const run = generationId !== undefined && activeRun?.runId === generationId ? activeRun : undefined;
8536
+ const operation = source === 'compaction' ? stored.metricsCompaction : run?.metricsOperation;
8537
+ if (!operation) throw new Error('Model call usage has no owning metrics operation.');
8538
+ observeSessionMetricsCall(operation, call);
8539
+ let scopeId = run?.scopeId;
8540
+ if (scopeId === undefined) {
8541
+ const ambientContext = this.compactorInvocationContext.getStore();
8542
+ const invocationContext = ambientContext
8543
+ ? this.deferredCompactorContexts.get(ambientContext) ?? ambientContext
8544
+ : undefined;
8545
+ if (
8546
+ !invocationContext
8547
+ || invocationContext.storageKey !== state.storageKey
8548
+ || invocationContext.sessionId !== sessionId
8549
+ ) {
8550
+ throw new Error('Model call usage is missing its exact FlexHarness invocation context.');
8551
+ }
8552
+ scopeId = invocationContext.scopeId;
8553
+ }
8554
+ if (run && call.status === 'reported') {
8555
+ for (const key of Object.keys(run.usage) as (keyof IFlexUsage)[]) run.usage[key] += call.usage[key];
8308
8556
  }
8309
- scopeId = invocationContext.scopeId;
8557
+ this.emitEvent(scopeId, stored.session, {
8558
+ type: 'model.usage',
8559
+ ...(generationId === undefined ? {} : { runId: generationId }),
8560
+ source,
8561
+ ...call,
8562
+ });
8563
+ } catch (error) {
8564
+ // AgentSession isolates the entire callback, including event/context failures. Join it later.
8565
+ stored.metricsFailure ??= error;
8566
+ throw error;
8310
8567
  }
8311
- if (run && call.status === 'reported') {
8312
- for (const key of Object.keys(run.usage) as (keyof IFlexUsage)[]) run.usage[key] += call.usage[key];
8568
+ }
8569
+
8570
+ private async settleMetricsOperation(
8571
+ state: IStorageState,
8572
+ stored: IStoredSessionState,
8573
+ operation: ISessionMetricsOperation,
8574
+ ): Promise<void> {
8575
+ try {
8576
+ if (stored.metricsFailure !== undefined) throw stored.metricsFailure;
8577
+ await this.mutateProjection(state, stored, () => settleSessionMetricsOperation(stored.metrics, operation), true);
8578
+ } catch (error) {
8579
+ stored.metricsFailure ??= error;
8580
+ // Never allow a later operation to overwrite a marker whose spend was not durably joined.
8581
+ state.lifecycle = 'fenced';
8582
+ throw error;
8313
8583
  }
8314
- this.emitEvent(scopeId, stored.session, {
8315
- type: 'model.usage',
8316
- ...(generationId === undefined ? {} : { runId: generationId }),
8317
- source,
8318
- ...call,
8319
- });
8320
8584
  }
8321
8585
 
8322
8586
  private callbackRun(state: IStorageState, sessionId: string): IActiveRun | undefined {
@@ -8820,9 +9084,12 @@ export class FlexHarness<TScope = unknown> {
8820
9084
  errors.push(this.projectExternalError(run, error, 'persistence'));
8821
9085
  }
8822
9086
  }
8823
- if (projectionReserved) {
9087
+ if (projectionReserved || run.stored.metrics.pendingGenerationId === run.runId) {
8824
9088
  try {
8825
9089
  await this.mutateProjection(run.state, run.stored, () => {
9090
+ if (run.stored.metrics.pendingGenerationId === run.runId) {
9091
+ delete run.stored.metrics.pendingGenerationId;
9092
+ }
8826
9093
  run.stored.messages = run.stored.messages.filter((message) => message.runId !== run.runId);
8827
9094
  run.stored.stagedTerminals = run.stored.stagedTerminals.filter((entry) => entry.runId !== run.runId);
8828
9095
  run.stored.reversionSegments = run.stored.reversionSegments.filter(
@@ -9403,6 +9670,7 @@ export class FlexHarness<TScope = unknown> {
9403
9670
  projectionReconciliationRequired: false,
9404
9671
  projectionRevision: 0,
9405
9672
  projectionQueue: Promise.resolve(),
9673
+ metrics: createSessionMetricsState(false),
9406
9674
  rememberedPermissionKeys: new Set(),
9407
9675
  permissionRevision: 0,
9408
9676
  permissionQueue: Promise.resolve(),
@@ -9427,6 +9695,7 @@ export class FlexHarness<TScope = unknown> {
9427
9695
  scopeId: string,
9428
9696
  scope: TScope,
9429
9697
  compactorLifecycleController = new AbortController(),
9698
+ newSession = false,
9430
9699
  ): Promise<IStoredSessionState> {
9431
9700
  const sessionId = metadata.sessionId;
9432
9701
  const compactorContext = this.createCompactorContext(
@@ -9530,11 +9799,12 @@ export class FlexHarness<TScope = unknown> {
9530
9799
  pendingReversionReleases,
9531
9800
  projectionSchemaVersion: projection?.schemaVersion ?? 3,
9532
9801
  projectionReconciliationRequired: false,
9533
- ...(projection && projection.schemaVersion !== 3
9802
+ ...(projection
9534
9803
  ? { projectionBaseline: cloneSerializable(projection) }
9535
9804
  : {}),
9536
9805
  projectionRevision: projection?.revision ?? 0,
9537
9806
  projectionQueue: Promise.resolve(),
9807
+ metrics: cloneSerializable(currentProjection?.metrics ?? createSessionMetricsState(newSession)),
9538
9808
  rememberedPermissionKeys: new Set(permission?.rememberedPermissionKeys ?? []),
9539
9809
  permissionRevision: permission?.revision ?? 0,
9540
9810
  permissionQueue: Promise.resolve(),
@@ -9553,6 +9823,19 @@ export class FlexHarness<TScope = unknown> {
9553
9823
  terminalPromptQueueEntries: new Map(),
9554
9824
  outstandingPromptBytes: 0,
9555
9825
  };
9826
+ if (currentProjection?.metrics === undefined
9827
+ || stored.metrics.pendingGenerationId !== undefined
9828
+ || stored.metrics.pendingCompactionId !== undefined
9829
+ || (metadata.origin !== undefined && stored.metrics.lifetimeUsageComplete)) {
9830
+ await this.mutateProjection(state, stored, () => {
9831
+ if (stored.metrics.pendingGenerationId !== undefined || stored.metrics.pendingCompactionId !== undefined
9832
+ || metadata.origin !== undefined) {
9833
+ stored.metrics.lifetimeUsageComplete = false;
9834
+ }
9835
+ delete stored.metrics.pendingGenerationId;
9836
+ delete stored.metrics.pendingCompactionId;
9837
+ });
9838
+ }
9556
9839
  const {
9557
9840
  contextCompactor,
9558
9841
  ...agentSessionPolicy
@@ -9609,22 +9892,55 @@ export class FlexHarness<TScope = unknown> {
9609
9892
  events: filteredEvents,
9610
9893
  }),
9611
9894
  );
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
- });
9895
+ const operation = createSessionMetricsOperation(plugins.crypto.randomUUID(), 'compaction');
9896
+ try {
9897
+ await this.mutateProjection(state, stored, () => {
9898
+ if (stored.metrics.pendingCompactionId !== undefined || stored.metricsFailure !== undefined) {
9899
+ throw new Error('Session metrics compaction ownership is unresolved.');
9900
+ }
9901
+ stored.metrics.pendingCompactionId = operation.id;
9902
+ });
9903
+ } catch (error) {
9904
+ // No compactor ran. Reconcile a known committed reservation without inventing spend.
9905
+ // Unknown outcomes remain fenced and are repaired conservatively on namespace reload.
9906
+ if (state.lifecycle !== 'fenced' && stored.metrics.pendingCompactionId === operation.id) {
9907
+ try {
9908
+ await this.mutateProjection(state, stored, () => {
9909
+ delete stored.metrics.pendingCompactionId;
9910
+ }, true);
9911
+ } catch (cleanupError) {
9912
+ stored.metricsFailure ??= cleanupError;
9913
+ state.lifecycle = 'fenced';
9914
+ throw combineErrors([error, cleanupError]);
9915
+ }
9916
+ }
9917
+ throw error;
9918
+ }
9919
+ stored.metricsCompaction = operation;
9920
+ try {
9921
+ return await contextCompactor(filteredMessages, filteredEvents, {
9922
+ ...options,
9923
+ ...invocation,
9924
+ scope: invocation.scope as TScope,
9925
+ ...this.compactionModel(state, sessionId, ambientContext, options.reason, modelHint),
9926
+ abortSignal: options.abortSignal
9927
+ ? AbortSignal.any([
9928
+ options.abortSignal,
9929
+ state.compactorLifecycleController.signal,
9930
+ stored.compactorLifecycleController.signal,
9931
+ ])
9932
+ : AbortSignal.any([
9933
+ state.compactorLifecycleController.signal,
9934
+ stored.compactorLifecycleController.signal,
9935
+ ]),
9936
+ });
9937
+ } finally {
9938
+ try {
9939
+ await this.settleMetricsOperation(state, stored, operation);
9940
+ } finally {
9941
+ delete stored.metricsCompaction;
9942
+ }
9943
+ }
9628
9944
  },
9629
9945
  }
9630
9946
  : {}),
@@ -10044,6 +10360,7 @@ export class FlexHarness<TScope = unknown> {
10044
10360
  const beforeBaseline = stored.projectionBaseline === undefined
10045
10361
  ? undefined
10046
10362
  : cloneSerializable(stored.projectionBaseline);
10363
+ const beforeMetrics = cloneSerializable(stored.metrics);
10047
10364
  const beforeMessages = cloneSerializable(stored.messages);
10048
10365
  const beforeStages = cloneSerializable(stored.stagedTerminals);
10049
10366
  const beforeSegments = cloneSerializable(stored.reversionSegments);
@@ -10056,6 +10373,7 @@ export class FlexHarness<TScope = unknown> {
10056
10373
  const beforeSnapshot: TFlexProjectionSnapshot = beforeBaseline ?? {
10057
10374
  schemaVersion: 3,
10058
10375
  revision: beforeRevision,
10376
+ metrics: cloneSerializable(beforeMetrics),
10059
10377
  messages: cloneSerializable(beforeMessages),
10060
10378
  stagedTerminals: cloneSerializable(beforeStages),
10061
10379
  reversionSegments: cloneSerializable(beforeSegments),
@@ -10072,6 +10390,7 @@ export class FlexHarness<TScope = unknown> {
10072
10390
  if (beforeBaseline === undefined) delete stored.projectionBaseline;
10073
10391
  else stored.projectionBaseline = beforeBaseline;
10074
10392
  stored.projectionRevision = beforeRevision;
10393
+ stored.metrics = beforeMetrics;
10075
10394
  stored.messages = beforeMessages;
10076
10395
  stored.stagedTerminals = beforeStages;
10077
10396
  stored.reversionSegments = beforeSegments;
@@ -10251,6 +10570,7 @@ export class FlexHarness<TScope = unknown> {
10251
10570
  return {
10252
10571
  schemaVersion: 3,
10253
10572
  revision,
10573
+ metrics: cloneSerializable(stored.metrics),
10254
10574
  messages: cloneSerializable(stored.messages),
10255
10575
  stagedTerminals: cloneSerializable(stored.stagedTerminals),
10256
10576
  reversionSegments: cloneSerializable(stored.reversionSegments),
@@ -10962,6 +11282,11 @@ export class FlexHarness<TScope = unknown> {
10962
11282
  errors.push(error);
10963
11283
  }
10964
11284
  }
11285
+ if (stored.metricsFailure !== undefined) {
11286
+ errors.push(stored.metricsFailure);
11287
+ // Runtime ownership has settled; the fenced namespace will be reloaded from its durable WAL.
11288
+ if (stored.agentSessionCloseCompleted) delete stored.metricsFailure;
11289
+ }
10965
11290
  if (errors.length > 0) throw combineErrors(errors);
10966
11291
  }
10967
11292