@modelprofile.com/flexharness 5.0.0 → 5.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.
@@ -160,7 +160,7 @@ export interface IFlexSlashCommandDescriptor {
160
160
  unavailableReason?: string;
161
161
  workspaceReversion: TFlexSlashCommandWorkspaceReversion;
162
162
  }
163
- export interface IFlexSlashCommandHandlerContext<TScope> {
163
+ export interface IFlexSlashCommandHandlerContext<TScope> extends IFlexSessionGeneration {
164
164
  readonly scopeId: string;
165
165
  readonly scope: TScope;
166
166
  readonly storageKey: string;
@@ -292,7 +292,7 @@ export interface IFlexPermissionRequest extends IFlexPermissionRequestInput {
292
292
  createdAt: string;
293
293
  }
294
294
  export type TFlexPermissionDecision = 'once' | 'always' | 'reject';
295
- export interface IFlexToolProviderContext<TScope> {
295
+ export interface IFlexToolProviderContext<TScope> extends IFlexSessionGeneration {
296
296
  scopeId: string;
297
297
  scope: TScope;
298
298
  sessionId: string;
@@ -309,7 +309,7 @@ export interface IFlexToolHandle {
309
309
  export interface IFlexToolProvider<TScope> {
310
310
  provideTools(context: IFlexToolProviderContext<TScope>): Promise<IFlexToolHandle | undefined> | IFlexToolHandle | undefined;
311
311
  }
312
- export interface IFlexResourceToolProviderResolverContext<TScope> {
312
+ export interface IFlexResourceToolProviderResolverContext<TScope> extends IFlexSessionGeneration {
313
313
  scopeId: string;
314
314
  scope: TScope;
315
315
  sessionId: string;
@@ -594,7 +594,7 @@ export declare const FLEX_REVERSION_MAX_AFFECTED_WORKSPACES = 64;
594
594
  export declare const FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES = 512;
595
595
  export declare const FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES = 2048;
596
596
  export declare const FLEX_REVERSION_REASON_CODE_MAX_BYTES = 128;
597
- export interface IFlexTurnReversionBaseContext<TScope> {
597
+ export interface IFlexTurnReversionBaseContext<TScope> extends IFlexSessionGeneration {
598
598
  readonly scopeId: string;
599
599
  readonly scope: TScope;
600
600
  readonly storageKey: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelprofile.com/flexharness",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "private": false,
5
5
  "description": "Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.",
6
6
  "main": "dist_ts/index.js",
package/readme.md CHANGED
@@ -122,7 +122,7 @@ const harness = new FlexHarness<IProjectScope>({
122
122
  });
123
123
  ```
124
124
 
125
- `modelRegistry`, `projectRegistry`, `createProjectTools`, and `closeProjectTools` in this example are application-owned integrations. FlexHarness passes the same run `AbortSignal` to the model resolver and tool provider.
125
+ `modelRegistry`, `projectRegistry`, `createProjectTools`, and `closeProjectTools` in this example are application-owned integrations. FlexHarness passes the same run `AbortSignal` to the model resolver and tool provider. Every application and resource tool-provider context also carries the required canonical `sessionGenerationId` and `sessionGenerationSequence`, allowing host operations to authorize the exact session generation rather than a reusable session ID alone.
126
126
 
127
127
  ## Resource Tool Providers
128
128
 
@@ -130,10 +130,19 @@ const harness = new FlexHarness<IProjectScope>({
130
130
 
131
131
  ```typescript
132
132
  resourceToolProviderResolver: {
133
- async resolveResourceToolProviders({ scope, sessionId, runId, signal }) {
133
+ async resolveResourceToolProviders({
134
+ scope,
135
+ sessionId,
136
+ sessionGenerationId,
137
+ sessionGenerationSequence,
138
+ runId,
139
+ signal,
140
+ }) {
134
141
  const attachments = await resourceRegistry.listAttached({
135
142
  scope,
136
143
  sessionId,
144
+ sessionGenerationId,
145
+ sessionGenerationSequence,
137
146
  runId,
138
147
  signal,
139
148
  });
@@ -146,7 +155,7 @@ resourceToolProviderResolver: {
146
155
  },
147
156
  ```
148
157
 
149
- Each descriptor uses the existing `IFlexToolProvider<TScope>` contract. Its provider receives the normal run context and must return a fresh run-scoped handle. The original `toolProvider` remains optional and its tool names remain unchanged. Resource tool names are deterministic and bounded:
158
+ Each descriptor uses the existing `IFlexToolProvider<TScope>` contract. Its provider receives the normal run context and must return a fresh run-scoped handle. The resource resolver context carries the same required canonical session generation as the tool-provider contexts. The original `toolProvider` remains optional and its tool names remain unchanged. Resource tool names are deterministic and bounded:
150
159
 
151
160
  1. `resourceIdentity` is the lowercase hexadecimal SHA-256 of `JSON.stringify([resourceId, attachmentRevision])`.
152
161
  2. The namespace is `resource_` plus the first 16 digest characters.
@@ -438,12 +447,24 @@ const harness = new FlexHarness({
438
447
  {
439
448
  name: 'refresh-index',
440
449
  description: 'Refresh the application-owned workspace index.',
441
- async handler({ scopeId, scope, storageKey, sessionId, rawArguments, arguments, signal }) {
450
+ async handler({
451
+ scopeId,
452
+ scope,
453
+ storageKey,
454
+ sessionId,
455
+ sessionGenerationId,
456
+ sessionGenerationSequence,
457
+ rawArguments,
458
+ arguments,
459
+ signal,
460
+ }) {
442
461
  return indexer.refresh({
443
462
  scopeId,
444
463
  scope,
445
464
  storageKey,
446
465
  sessionId,
466
+ sessionGenerationId,
467
+ sessionGenerationSequence,
447
468
  rawArguments,
448
469
  arguments,
449
470
  signal,
@@ -470,7 +491,7 @@ Session metadata archival through `updateSession(..., { archived: true })` only
470
491
 
471
492
  Templates replace every `$ARGUMENTS` with untouched raw argument text. `$1` through the highest referenced positional placeholder use tokenized arguments, with the highest position receiving all remaining tokens joined by spaces. Missing positions become empty. A template with no placeholders appends non-empty raw arguments after a blank line. `/init` uses the OpenCode 1.18.15 `AGENTS.md` initialization prompt with provider-neutral active-workspace wording.
472
493
 
473
- Handler context is frozen and contains only the resolved scope identity, session identity, raw and tokenized arguments, and an `AbortSignal`. Handler results are converted with the configured `toolOutputLimits`; `void` becomes JSON `null`. Handler failures use `externalErrorProjector` with source `slashCommand`. Same-session command overlap is rejected, including reentry from a handler. Scope retirement and disposal abort and await active handlers; prompt-admission commands transfer immediately to the normal prompt lifecycle.
494
+ Handler context is frozen and contains only the resolved scope identity, session identity, required canonical `sessionGenerationId` and `sessionGenerationSequence`, raw and tokenized arguments, and an `AbortSignal`. Handler results are converted with the configured `toolOutputLimits`; `void` becomes JSON `null`. Handler failures use `externalErrorProjector` with source `slashCommand`. Same-session command overlap is rejected, including reentry from a handler. Scope retirement and disposal abort and await active handlers; prompt-admission commands transfer immediately to the normal prompt lifecycle.
474
495
 
475
496
  ### Workspace Reversion Provider
476
497
 
@@ -557,7 +578,7 @@ for (const group of info.groups) {
557
578
 
558
579
  The immutable result exposes `undoAvailable`, `redoAvailable`, and groups classified as `candidate`, `barrier`, or `no-change`. Group metadata contains at most 64 unique affected workspaces; `affectedWorkspacesTruncated` is `true` when additional unique descriptors were omitted. It never includes capture IDs or provider references.
559
580
 
560
- `workspaceSnapshots` is application-owned. Every context contains `scopeId`, `scope`, `storageKey`, `sessionId`, `runId`, deterministic `captureId`, and an `AbortSignal`. Apply contexts additionally contain the normalized `reference`, deterministic per-segment `operationId`, and `direction`; release contexts contain the reference.
581
+ `workspaceSnapshots` is application-owned. Every context contains `scopeId`, `scope`, `storageKey`, `sessionId`, the required canonical `sessionGenerationId` and `sessionGenerationSequence`, `runId`, deterministic `captureId`, and an `AbortSignal`. Apply contexts additionally contain the normalized `reference`, deterministic per-segment `operationId`, and `direction`; release contexts contain the reference. Deleted-session recovery retains the same generation from the durable tombstone, so providers can reject callbacks from an older same-ID generation.
561
582
 
562
583
  The protocol is durable and inspectable:
563
584
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/flexharness',
6
- version: '5.0.0',
6
+ version: '5.1.0',
7
7
  description: 'Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.'
8
8
  }
@@ -74,6 +74,7 @@ import type {
74
74
  IFlexSchedulePromptOptions,
75
75
  IFlexScopeSnapshot,
76
76
  IFlexSession,
77
+ IFlexSessionGeneration,
77
78
  IFlexSessionReversionGroup,
78
79
  IFlexSessionReversionInfo,
79
80
  IFlexSessionTombstone,
@@ -519,7 +520,7 @@ function requireSessionGeneration(
519
520
  IFlexSession | IFlexSessionTombstone,
520
521
  'sessionGenerationId' | 'sessionGenerationSequence'
521
522
  >,
522
- ): { sessionGenerationId: string; sessionGenerationSequence: number } {
523
+ ): IFlexSessionGeneration {
523
524
  if (
524
525
  !session.sessionGenerationId
525
526
  || !Number.isSafeInteger(session.sessionGenerationSequence)
@@ -1849,6 +1850,7 @@ export class FlexHarness<TScope = unknown> {
1849
1850
  scope,
1850
1851
  storageKey: state.storageKey,
1851
1852
  sessionId: stored.session.sessionId,
1853
+ ...requireSessionGeneration(stored.session),
1852
1854
  rawArguments,
1853
1855
  arguments: arguments_,
1854
1856
  signal,
@@ -2512,6 +2514,7 @@ export class FlexHarness<TScope = unknown> {
2512
2514
  ): Promise<void> {
2513
2515
  const pending = stored.pendingReversion;
2514
2516
  if (pending?.kind !== 'apply') return;
2517
+ const sessionGeneration = requireSessionGeneration(stored.session);
2515
2518
  const segmentMap = new Map(stored.reversionSegments.map((segment) => [segment.runId, segment]));
2516
2519
  if (
2517
2520
  pending.segmentRunIds.some((runId) =>
@@ -2552,6 +2555,7 @@ export class FlexHarness<TScope = unknown> {
2552
2555
  scope,
2553
2556
  storageKey: state.storageKey,
2554
2557
  sessionId: stored.session.sessionId,
2558
+ ...sessionGeneration,
2555
2559
  runId,
2556
2560
  captureId,
2557
2561
  reference: cloneSerializable(reference),
@@ -2685,6 +2689,7 @@ export class FlexHarness<TScope = unknown> {
2685
2689
  scopeId,
2686
2690
  scope,
2687
2691
  stored.session.sessionId,
2692
+ requireSessionGeneration(stored.session),
2688
2693
  pending.runId,
2689
2694
  pending.captureId,
2690
2695
  pending.state === 'prepared' ? 'finalizing' : pending.state,
@@ -3999,6 +4004,7 @@ export class FlexHarness<TScope = unknown> {
3999
4004
  scopeId: run.scopeId,
4000
4005
  scope: run.scope as TScope,
4001
4006
  sessionId: run.sessionId,
4007
+ ...requireSessionGeneration(run.stored.session),
4002
4008
  runId: run.runId,
4003
4009
  ...resolverRelationship,
4004
4010
  signal,
@@ -4967,6 +4973,7 @@ export class FlexHarness<TScope = unknown> {
4967
4973
  scope: run.scope as TScope,
4968
4974
  storageKey: run.state.storageKey,
4969
4975
  sessionId: run.sessionId,
4976
+ ...requireSessionGeneration(run.stored.session),
4970
4977
  runId: run.runId,
4971
4978
  captureId,
4972
4979
  signal,
@@ -5001,6 +5008,7 @@ export class FlexHarness<TScope = unknown> {
5001
5008
  scope: TScope,
5002
5009
  storageKey: string,
5003
5010
  sessionId: string,
5011
+ sessionGeneration: Readonly<IFlexSessionGeneration>,
5004
5012
  runId: string,
5005
5013
  captureId: string,
5006
5014
  ): Promise<TNormalizedCaptureInspection> {
@@ -5011,6 +5019,7 @@ export class FlexHarness<TScope = unknown> {
5011
5019
  scope,
5012
5020
  storageKey,
5013
5021
  sessionId,
5022
+ ...sessionGeneration,
5014
5023
  runId,
5015
5024
  captureId,
5016
5025
  signal,
@@ -5044,6 +5053,7 @@ export class FlexHarness<TScope = unknown> {
5044
5053
  scope: TScope,
5045
5054
  storageKey: string,
5046
5055
  sessionId: string,
5056
+ sessionGeneration: Readonly<IFlexSessionGeneration>,
5047
5057
  runId: string,
5048
5058
  captureId: string,
5049
5059
  ): Promise<TFlexTurnReversionFinalizedOutcome> {
@@ -5053,6 +5063,7 @@ export class FlexHarness<TScope = unknown> {
5053
5063
  scope,
5054
5064
  storageKey,
5055
5065
  sessionId,
5066
+ ...sessionGeneration,
5056
5067
  runId,
5057
5068
  captureId,
5058
5069
  signal,
@@ -5072,6 +5083,7 @@ export class FlexHarness<TScope = unknown> {
5072
5083
  scopeId: string,
5073
5084
  scope: TScope,
5074
5085
  sessionId: string,
5086
+ sessionGeneration: Readonly<IFlexSessionGeneration>,
5075
5087
  runId: string,
5076
5088
  captureId: string,
5077
5089
  durableState: 'preparing' | 'prepared' | 'finalizing',
@@ -5083,6 +5095,7 @@ export class FlexHarness<TScope = unknown> {
5083
5095
  scope,
5084
5096
  state.storageKey,
5085
5097
  sessionId,
5098
+ sessionGeneration,
5086
5099
  runId,
5087
5100
  captureId,
5088
5101
  );
@@ -5107,6 +5120,7 @@ export class FlexHarness<TScope = unknown> {
5107
5120
  scope,
5108
5121
  state.storageKey,
5109
5122
  sessionId,
5123
+ sessionGeneration,
5110
5124
  runId,
5111
5125
  captureId,
5112
5126
  );
@@ -5116,6 +5130,7 @@ export class FlexHarness<TScope = unknown> {
5116
5130
  scope,
5117
5131
  state.storageKey,
5118
5132
  sessionId,
5133
+ sessionGeneration,
5119
5134
  runId,
5120
5135
  captureId,
5121
5136
  );
@@ -5268,6 +5283,7 @@ export class FlexHarness<TScope = unknown> {
5268
5283
  run.scope as TScope,
5269
5284
  run.state.storageKey,
5270
5285
  run.sessionId,
5286
+ requireSessionGeneration(run.stored.session),
5271
5287
  run.runId,
5272
5288
  captureId,
5273
5289
  );
@@ -5349,6 +5365,7 @@ export class FlexHarness<TScope = unknown> {
5349
5365
  run.scopeId,
5350
5366
  run.scope as TScope,
5351
5367
  run.sessionId,
5368
+ requireSessionGeneration(run.stored.session),
5352
5369
  run.runId,
5353
5370
  captureId,
5354
5371
  pending.state === 'prepared' ? 'finalizing' : pending.state,
@@ -5598,6 +5615,7 @@ export class FlexHarness<TScope = unknown> {
5598
5615
  }
5599
5616
  return;
5600
5617
  }
5618
+ const sessionGeneration = requireSessionGeneration(stored.session);
5601
5619
  while (stored.pendingReversionReleases.length > 0) {
5602
5620
  const release = stored.pendingReversionReleases[0];
5603
5621
  if (this.reversionProviderProtocolVersion() !== release.protocolVersion) {
@@ -5612,6 +5630,7 @@ export class FlexHarness<TScope = unknown> {
5612
5630
  scope,
5613
5631
  storageKey: state.storageKey,
5614
5632
  sessionId: stored.session.sessionId,
5633
+ ...sessionGeneration,
5615
5634
  runId: release.runId,
5616
5635
  captureId: release.captureId,
5617
5636
  reference: cloneSerializable(release.reference),
@@ -5649,11 +5668,13 @@ export class FlexHarness<TScope = unknown> {
5649
5668
  private async releaseDeletedSessionReversions(
5650
5669
  state: IStorageState,
5651
5670
  storageKey: string,
5652
- sessionId: string,
5671
+ tombstone: IFlexSessionTombstone,
5653
5672
  scopeId: string,
5654
5673
  scope: TScope,
5655
5674
  stored?: IStoredSessionState,
5656
5675
  ): Promise<void> {
5676
+ const sessionId = tombstone.sessionId;
5677
+ const sessionGeneration = requireSessionGeneration(stored?.session ?? tombstone);
5657
5678
  if (stored) await stored.projectionQueue;
5658
5679
  let durableProjection = await this.stores.projections.load(storageKey, sessionId);
5659
5680
  if (!durableProjection) return;
@@ -5712,6 +5733,7 @@ export class FlexHarness<TScope = unknown> {
5712
5733
  scope,
5713
5734
  storageKey,
5714
5735
  sessionId,
5736
+ ...sessionGeneration,
5715
5737
  runId,
5716
5738
  captureId,
5717
5739
  reference: cloneSerializable(reference),
@@ -5825,6 +5847,7 @@ export class FlexHarness<TScope = unknown> {
5825
5847
  scope,
5826
5848
  storageKey,
5827
5849
  sessionId,
5850
+ sessionGeneration,
5828
5851
  capture.runId,
5829
5852
  capture.captureId,
5830
5853
  );
@@ -5854,6 +5877,7 @@ export class FlexHarness<TScope = unknown> {
5854
5877
  scope,
5855
5878
  storageKey,
5856
5879
  sessionId,
5880
+ sessionGeneration,
5857
5881
  capture.runId,
5858
5882
  capture.captureId,
5859
5883
  );
@@ -5863,6 +5887,7 @@ export class FlexHarness<TScope = unknown> {
5863
5887
  scope,
5864
5888
  storageKey,
5865
5889
  sessionId,
5890
+ sessionGeneration,
5866
5891
  capture.runId,
5867
5892
  capture.captureId,
5868
5893
  );
@@ -5928,6 +5953,7 @@ export class FlexHarness<TScope = unknown> {
5928
5953
  scope,
5929
5954
  storageKey,
5930
5955
  sessionId,
5956
+ ...sessionGeneration,
5931
5957
  runId: release.runId,
5932
5958
  captureId: release.captureId,
5933
5959
  reference: cloneSerializable(release.reference),
@@ -6707,6 +6733,7 @@ export class FlexHarness<TScope = unknown> {
6707
6733
  scopeId: context.scopeId,
6708
6734
  scope: context.scope,
6709
6735
  sessionId: context.sessionId,
6736
+ ...requireSessionGeneration(run.stored.session),
6710
6737
  runId: context.runId,
6711
6738
  ...(context.parentSessionId === undefined ? {} : { parentSessionId: context.parentSessionId }),
6712
6739
  ...(context.agent === undefined ? {} : { agent: context.agent }),
@@ -7116,7 +7143,7 @@ export class FlexHarness<TScope = unknown> {
7116
7143
  await this.releaseDeletedSessionReversions(
7117
7144
  state,
7118
7145
  storageKey,
7119
- tombstone.sessionId,
7146
+ tombstone,
7120
7147
  scopeId,
7121
7148
  scope,
7122
7149
  );
@@ -8362,7 +8389,7 @@ export class FlexHarness<TScope = unknown> {
8362
8389
  await this.releaseDeletedSessionReversions(
8363
8390
  state,
8364
8391
  state.storageKey,
8365
- sessionId,
8392
+ tombstone,
8366
8393
  releaseContext.scopeId,
8367
8394
  releaseContext.scope,
8368
8395
  retained?.stored,
package/ts/interfaces.ts CHANGED
@@ -207,7 +207,7 @@ export interface IFlexSlashCommandDescriptor {
207
207
  workspaceReversion: TFlexSlashCommandWorkspaceReversion;
208
208
  }
209
209
 
210
- export interface IFlexSlashCommandHandlerContext<TScope> {
210
+ export interface IFlexSlashCommandHandlerContext<TScope> extends IFlexSessionGeneration {
211
211
  readonly scopeId: string;
212
212
  readonly scope: TScope;
213
213
  readonly storageKey: string;
@@ -386,7 +386,7 @@ export interface IFlexPermissionRequest extends IFlexPermissionRequestInput {
386
386
 
387
387
  export type TFlexPermissionDecision = 'once' | 'always' | 'reject';
388
388
 
389
- export interface IFlexToolProviderContext<TScope> {
389
+ export interface IFlexToolProviderContext<TScope> extends IFlexSessionGeneration {
390
390
  scopeId: string;
391
391
  scope: TScope;
392
392
  sessionId: string;
@@ -408,7 +408,7 @@ export interface IFlexToolProvider<TScope> {
408
408
  ): Promise<IFlexToolHandle | undefined> | IFlexToolHandle | undefined;
409
409
  }
410
410
 
411
- export interface IFlexResourceToolProviderResolverContext<TScope> {
411
+ export interface IFlexResourceToolProviderResolverContext<TScope> extends IFlexSessionGeneration {
412
412
  scopeId: string;
413
413
  scope: TScope;
414
414
  sessionId: string;
@@ -793,7 +793,7 @@ export const FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES = 512;
793
793
  export const FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES = 2048;
794
794
  export const FLEX_REVERSION_REASON_CODE_MAX_BYTES = 128;
795
795
 
796
- export interface IFlexTurnReversionBaseContext<TScope> {
796
+ export interface IFlexTurnReversionBaseContext<TScope> extends IFlexSessionGeneration {
797
797
  readonly scopeId: string;
798
798
  readonly scope: TScope;
799
799
  readonly storageKey: string;