@modelprofile.com/flexharness 3.8.0 → 4.0.1

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.
@@ -10,12 +10,17 @@ import type {
10
10
  IFlexHarnessStores,
11
11
  IFlexPermissionSnapshot,
12
12
  IFlexPermissionStore,
13
+ IFlexProjectManagementSnapshot,
14
+ IFlexProjectManagementStore,
15
+ IFlexProjectManagementTombstone,
16
+ IFlexProjectManagementWriteContext,
13
17
  IFlexProjectionSnapshotV3,
14
18
  IFlexProjectionStore,
15
19
  IFlexScopeSnapshot,
16
20
  IFlexScopeStore,
17
21
  IFlexToolJobStoreProvider,
18
22
  TFlexProjectionSnapshot,
23
+ TFlexProjectManagementRecord,
19
24
  } from './interfaces.js';
20
25
  import {
21
26
  assertFlexPermissionSnapshot,
@@ -24,8 +29,17 @@ import {
24
29
  assertJsonSerializable,
25
30
  cloneSerializable,
26
31
  } from './utils.json.js';
27
-
28
- type TSnapshot = IFlexScopeSnapshot | TFlexProjectionSnapshot | IFlexPermissionSnapshot;
32
+ import {
33
+ assertFlexProjectManagementRecord,
34
+ assertFlexProjectManagementSnapshot,
35
+ assertFlexProjectManagementTombstone,
36
+ } from './utils.projectmanagement.js';
37
+
38
+ type TSnapshot =
39
+ | IFlexScopeSnapshot
40
+ | TFlexProjectionSnapshot
41
+ | IFlexPermissionSnapshot
42
+ | TFlexProjectManagementRecord;
29
43
  type TSnapshotValidator<TSnapshotValue extends TSnapshot> = (
30
44
  value: unknown,
31
45
  ) => asserts value is TSnapshotValue;
@@ -42,6 +56,20 @@ function validateExpectedRevision(expectedRevision: number): void {
42
56
  }
43
57
  }
44
58
 
59
+ function validateProjectManagementWriteContext(
60
+ writeContext: IFlexProjectManagementWriteContext,
61
+ ): void {
62
+ assertJsonSerializable(writeContext, '$writeContext');
63
+ assertRecord(writeContext, 'writeContext');
64
+ assertExactKeys(writeContext, ['actor', 'runId', 'agent', 'toolCallId'], 'writeContext');
65
+ if (writeContext.actor !== 'agent' && writeContext.actor !== 'application') {
66
+ throw new FlexHarnessValidationError('writeContext.actor must be agent or application.');
67
+ }
68
+ for (const key of ['runId', 'agent', 'toolCallId'] as const) {
69
+ if (writeContext[key] !== undefined) validateIdentifier(writeContext[key], `writeContext.${key}`);
70
+ }
71
+ }
72
+
45
73
  function validateSnapshotSave<TSnapshotValue extends TSnapshot>(
46
74
  snapshot: TSnapshotValue,
47
75
  expectedRevision: number,
@@ -63,6 +91,25 @@ function providerKey(storageKey: string, sessionId: string): string {
63
91
  return JSON.stringify([storageKey, sessionId]);
64
92
  }
65
93
 
94
+ function sameProjectManagementGeneration(
95
+ left: TFlexProjectManagementRecord,
96
+ right: TFlexProjectManagementRecord,
97
+ ): boolean {
98
+ return left.sessionGenerationId === right.sessionGenerationId
99
+ && left.sessionGenerationSequence === right.sessionGenerationSequence;
100
+ }
101
+
102
+ function canInitializeProjectManagementGeneration(
103
+ current: TFlexProjectManagementRecord,
104
+ next: TFlexProjectManagementRecord,
105
+ expectedRevision: number,
106
+ ): boolean {
107
+ return 'deletedAt' in current
108
+ && expectedRevision === 0
109
+ && next.sessionGenerationId !== current.sessionGenerationId
110
+ && next.sessionGenerationSequence > current.sessionGenerationSequence;
111
+ }
112
+
66
113
  function assertProjectionSession(snapshot: TFlexProjectionSnapshot, sessionId: string): void {
67
114
  for (const message of snapshot.messages) {
68
115
  if (message.sessionId !== sessionId) {
@@ -414,6 +461,81 @@ class InMemoryPermissionStore implements IFlexPermissionStore {
414
461
  }
415
462
  }
416
463
 
464
+ export class InMemoryFlexProjectManagementStore implements IFlexProjectManagementStore {
465
+ private readonly snapshots = new Map<string, TFlexProjectManagementRecord>();
466
+
467
+ public async load(
468
+ storageKey: string,
469
+ sessionId: string,
470
+ ): Promise<TFlexProjectManagementRecord | undefined> {
471
+ const snapshot = this.snapshots.get(providerKey(storageKey, sessionId));
472
+ return snapshot ? cloneSerializable(snapshot) : undefined;
473
+ }
474
+
475
+ public async save(
476
+ storageKey: string,
477
+ sessionId: string,
478
+ snapshot: IFlexProjectManagementSnapshot,
479
+ expectedRevision: number,
480
+ writeContext: IFlexProjectManagementWriteContext,
481
+ ): Promise<void> {
482
+ const key = providerKey(storageKey, sessionId);
483
+ validateProjectManagementWriteContext(writeContext);
484
+ const validated = validateSnapshotSave(
485
+ snapshot,
486
+ expectedRevision,
487
+ assertFlexProjectManagementSnapshot,
488
+ );
489
+ const current = this.snapshots.get(key);
490
+ const actualRevision = current?.revision ?? 0;
491
+ if (current && !sameProjectManagementGeneration(current, validated)) {
492
+ if (!canInitializeProjectManagementGeneration(current, validated, expectedRevision)) {
493
+ throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
494
+ }
495
+ } else if ((current && 'deletedAt' in current) || actualRevision !== expectedRevision) {
496
+ throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
497
+ }
498
+ this.snapshots.set(key, validated);
499
+ }
500
+
501
+ public async tombstoneSession(
502
+ storageKey: string,
503
+ sessionId: string,
504
+ tombstone: IFlexProjectManagementTombstone,
505
+ expectedRevision: number,
506
+ ): Promise<void> {
507
+ const key = providerKey(storageKey, sessionId);
508
+ const validated = validateSnapshotSave(
509
+ tombstone,
510
+ expectedRevision,
511
+ assertFlexProjectManagementTombstone,
512
+ );
513
+ const current = this.snapshots.get(key);
514
+ const actualRevision = current?.revision ?? 0;
515
+ if (current && sameProjectManagementGeneration(current, validated)) {
516
+ if ('deletedAt' in current) return;
517
+ if (actualRevision !== expectedRevision) {
518
+ throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
519
+ }
520
+ } else if (current) {
521
+ if (!canInitializeProjectManagementGeneration(current, validated, expectedRevision)) {
522
+ throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
523
+ }
524
+ } else if (actualRevision !== expectedRevision) {
525
+ throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
526
+ }
527
+ this.snapshots.set(key, validated);
528
+ }
529
+
530
+ public async purgeNamespace(storageKey: string): Promise<void> {
531
+ validateIdentifier(storageKey, 'storageKey');
532
+ for (const key of this.snapshots.keys()) {
533
+ const [recordStorageKey] = JSON.parse(key) as [string, string];
534
+ if (recordStorageKey === storageKey) this.snapshots.delete(key);
535
+ }
536
+ }
537
+ }
538
+
417
539
  class InMemoryAgentEventStore implements plugins.IAgentEventStoreV2 {
418
540
  public readonly eventSchemaVersion = 2 as const;
419
541
  private snapshot: plugins.IAgentEventSnapshotV2 | undefined;
@@ -563,9 +685,18 @@ export class InMemoryFlexHarnessStores implements IFlexHarnessStores {
563
685
  public readonly permissions: IFlexPermissionStore = new InMemoryPermissionStore();
564
686
  public readonly agentEvents: IFlexAgentEventStoreProvider = new InMemoryAgentEventStoreProvider();
565
687
  public readonly jobs: IFlexToolJobStoreProvider = new InMemoryToolJobStoreProvider();
688
+ public readonly projectManagement = new InMemoryFlexProjectManagementStore();
566
689
  }
567
690
 
568
- const jsonDomains = ['scopes', 'projections', 'permissions', 'events', 'archives', 'jobs'] as const;
691
+ const jsonDomains = [
692
+ 'scopes',
693
+ 'projections',
694
+ 'permissions',
695
+ 'projectManagement',
696
+ 'events',
697
+ 'archives',
698
+ 'jobs',
699
+ ] as const;
569
700
  type TJsonDomain = (typeof jsonDomains)[number];
570
701
 
571
702
  interface IDirectorySyncAttempt {
@@ -621,6 +752,11 @@ class JsonFileRoot {
621
752
  );
622
753
  }
623
754
 
755
+ public sessionNamespacePath(domain: TJsonDomain, storageKey: string): string {
756
+ validateIdentifier(storageKey, 'storageKey');
757
+ return plugins.path.join(this.directory, domain, this.digest(storageKey));
758
+ }
759
+
624
760
  public archivePath(storageKey: string, sessionId: string, archiveId: string): string {
625
761
  validateIdentifier(archiveId, 'archiveId');
626
762
  return plugins.path.join(
@@ -1058,6 +1194,127 @@ class JsonPermissionStore implements IFlexPermissionStore {
1058
1194
  }
1059
1195
  }
1060
1196
 
1197
+ class JsonProjectManagementStore implements IFlexProjectManagementStore {
1198
+ constructor(private readonly root: JsonFileRoot) {}
1199
+
1200
+ private inNamespaceQueue<T>(storageKey: string, operation: () => Promise<T>): Promise<T> {
1201
+ return JsonFileRoot.inQueue(
1202
+ `project-management:${this.root.sessionNamespacePath('projectManagement', storageKey)}`,
1203
+ operation,
1204
+ );
1205
+ }
1206
+
1207
+ public async load(
1208
+ storageKey: string,
1209
+ sessionId: string,
1210
+ ): Promise<TFlexProjectManagementRecord | undefined> {
1211
+ const filePath = this.root.sessionPath('projectManagement', storageKey, sessionId);
1212
+ return this.inNamespaceQueue(storageKey, () =>
1213
+ JsonFileRoot.inQueue(filePath, async () => {
1214
+ const snapshot = await this.root.read(filePath, (value) => {
1215
+ assertFlexProjectManagementRecord(value);
1216
+ return value;
1217
+ });
1218
+ return snapshot ? cloneSerializable(snapshot) : undefined;
1219
+ })
1220
+ );
1221
+ }
1222
+
1223
+ public async save(
1224
+ storageKey: string,
1225
+ sessionId: string,
1226
+ snapshot: IFlexProjectManagementSnapshot,
1227
+ expectedRevision: number,
1228
+ writeContext: IFlexProjectManagementWriteContext,
1229
+ ): Promise<void> {
1230
+ validateProjectManagementWriteContext(writeContext);
1231
+ const validated = validateSnapshotSave(
1232
+ snapshot,
1233
+ expectedRevision,
1234
+ assertFlexProjectManagementSnapshot,
1235
+ );
1236
+ const filePath = this.root.sessionPath('projectManagement', storageKey, sessionId);
1237
+ await this.inNamespaceQueue(storageKey, () =>
1238
+ JsonFileRoot.inQueue(filePath, async () => {
1239
+ const current = await this.root.read(filePath, (value) => {
1240
+ assertFlexProjectManagementRecord(value);
1241
+ return value;
1242
+ });
1243
+ const actualRevision = current?.revision ?? 0;
1244
+ if (current && !sameProjectManagementGeneration(current, validated)) {
1245
+ if (!canInitializeProjectManagementGeneration(current, validated, expectedRevision)) {
1246
+ throw new FlexHarnessStoreConflictError(
1247
+ providerKey(storageKey, sessionId),
1248
+ expectedRevision,
1249
+ actualRevision,
1250
+ );
1251
+ }
1252
+ } else if ((current && 'deletedAt' in current) || actualRevision !== expectedRevision) {
1253
+ throw new FlexHarnessStoreConflictError(
1254
+ providerKey(storageKey, sessionId),
1255
+ expectedRevision,
1256
+ actualRevision,
1257
+ );
1258
+ }
1259
+ await this.root.write(filePath, validated);
1260
+ }),
1261
+ );
1262
+ }
1263
+
1264
+ public async tombstoneSession(
1265
+ storageKey: string,
1266
+ sessionId: string,
1267
+ tombstone: IFlexProjectManagementTombstone,
1268
+ expectedRevision: number,
1269
+ ): Promise<void> {
1270
+ const validated = validateSnapshotSave(
1271
+ tombstone,
1272
+ expectedRevision,
1273
+ assertFlexProjectManagementTombstone,
1274
+ );
1275
+ const filePath = this.root.sessionPath('projectManagement', storageKey, sessionId);
1276
+ await this.inNamespaceQueue(storageKey, () =>
1277
+ JsonFileRoot.inQueue(filePath, async () => {
1278
+ const current = await this.root.read(filePath, (value) => {
1279
+ assertFlexProjectManagementRecord(value);
1280
+ return value;
1281
+ });
1282
+ const actualRevision = current?.revision ?? 0;
1283
+ if (current && sameProjectManagementGeneration(current, validated)) {
1284
+ if ('deletedAt' in current) return;
1285
+ if (actualRevision !== expectedRevision) {
1286
+ throw new FlexHarnessStoreConflictError(
1287
+ providerKey(storageKey, sessionId),
1288
+ expectedRevision,
1289
+ actualRevision,
1290
+ );
1291
+ }
1292
+ } else if (current) {
1293
+ if (!canInitializeProjectManagementGeneration(current, validated, expectedRevision)) {
1294
+ throw new FlexHarnessStoreConflictError(
1295
+ providerKey(storageKey, sessionId),
1296
+ expectedRevision,
1297
+ actualRevision,
1298
+ );
1299
+ }
1300
+ } else if (actualRevision !== expectedRevision) {
1301
+ throw new FlexHarnessStoreConflictError(
1302
+ providerKey(storageKey, sessionId),
1303
+ expectedRevision,
1304
+ actualRevision,
1305
+ );
1306
+ }
1307
+ await this.root.write(filePath, validated);
1308
+ }),
1309
+ );
1310
+ }
1311
+
1312
+ public async purgeNamespace(storageKey: string): Promise<void> {
1313
+ const directory = this.root.sessionNamespacePath('projectManagement', storageKey);
1314
+ await this.inNamespaceQueue(storageKey, () => this.root.deleteDirectory(directory));
1315
+ }
1316
+ }
1317
+
1061
1318
  async function clearJsonAgentEventSession(
1062
1319
  root: JsonFileRoot,
1063
1320
  storageKey: string,
@@ -1305,6 +1562,7 @@ export class JsonFileFlexHarnessStores implements IFlexHarnessStores {
1305
1562
  public readonly permissions: IFlexPermissionStore;
1306
1563
  public readonly agentEvents: IFlexAgentEventStoreProvider;
1307
1564
  public readonly jobs: IFlexToolJobStoreProvider;
1565
+ public readonly projectManagement: IFlexProjectManagementStore;
1308
1566
  private readonly root: JsonFileRoot;
1309
1567
 
1310
1568
  constructor(options: IJsonFileFlexHarnessStoresOptions) {
@@ -1315,6 +1573,7 @@ export class JsonFileFlexHarnessStores implements IFlexHarnessStores {
1315
1573
  this.scopes = new JsonScopeStore(this.root);
1316
1574
  this.projections = new JsonProjectionStore(this.root);
1317
1575
  this.permissions = new JsonPermissionStore(this.root);
1576
+ this.projectManagement = new JsonProjectManagementStore(this.root);
1318
1577
  this.agentEvents = new JsonAgentEventStoreProvider(this.root);
1319
1578
  this.jobs = new JsonToolJobStoreProvider(this.root);
1320
1579
  }
package/ts/index.ts CHANGED
@@ -3,6 +3,12 @@ export * from './classes.stores.js';
3
3
  export * from './errors.js';
4
4
  export * from './interfaces.js';
5
5
  export { normalizeJsonValue } from './utils.json.js';
6
+ export {
7
+ assertFlexProjectManagementSnapshot,
8
+ assertFlexProjectManagementRecord,
9
+ assertFlexProjectManagementTombstone,
10
+ createEmptyFlexProjectManagementSnapshot,
11
+ } from './utils.projectmanagement.js';
6
12
  export {
7
13
  FLEX_SLASH_COMMAND_MAX_INPUT_BYTES,
8
14
  parseSlashCommand,
package/ts/interfaces.ts CHANGED
@@ -135,6 +135,8 @@ export interface IFlexSessionActivity {
135
135
  export interface IFlexSession {
136
136
  scopeId: string;
137
137
  sessionId: string;
138
+ sessionGenerationId?: string;
139
+ sessionGenerationSequence?: number;
138
140
  title?: string;
139
141
  createdAt: string;
140
142
  updatedAt: string;
@@ -428,6 +430,8 @@ export interface IFlexResourceToolProviderResolver<TScope> {
428
430
 
429
431
  export interface IFlexSessionTombstone {
430
432
  sessionId: string;
433
+ sessionGenerationId?: string;
434
+ sessionGenerationSequence?: number;
431
435
  deletedAt: string;
432
436
  rootSessionId?: string;
433
437
  depth?: number;
@@ -564,6 +568,118 @@ export interface IFlexPermissionSnapshot {
564
568
  rememberedPermissionKeys: string[];
565
569
  }
566
570
 
571
+ export const FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION = 1 as const;
572
+ export const FLEX_SESSION_GENERATION_ID_MAX_BYTES = 128;
573
+
574
+ export const FLEX_PROJECT_MANAGEMENT_LIMITS = Object.freeze({
575
+ maxGoalBytes: 8 * 1024,
576
+ maxScratchpadBytes: 128 * 1024,
577
+ maxTaskContentBytes: 8 * 1024,
578
+ maxTaskIdBytes: 512,
579
+ maxTasks: 512,
580
+ maxTitleBytes: 2048,
581
+ maxSnapshotBytes: 1024 * 1024,
582
+ });
583
+
584
+ export type TFlexProjectTaskStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';
585
+ export type TFlexProjectTaskPriority = 'high' | 'medium' | 'low';
586
+
587
+ export interface IFlexProjectTask {
588
+ id: string;
589
+ content: string;
590
+ status: TFlexProjectTaskStatus;
591
+ priority: TFlexProjectTaskPriority;
592
+ createdAt: string;
593
+ updatedAt: string;
594
+ }
595
+
596
+ export interface IFlexProjectManagementSnapshot {
597
+ schemaVersion: 1;
598
+ revision: number;
599
+ sessionGenerationId: string;
600
+ sessionGenerationSequence: number;
601
+ goal?: string;
602
+ scratchpad: string;
603
+ tasks: IFlexProjectTask[];
604
+ }
605
+
606
+ export interface IFlexProjectManagementTombstone {
607
+ schemaVersion: 1;
608
+ revision: number;
609
+ sessionGenerationId: string;
610
+ sessionGenerationSequence: number;
611
+ deletedAt: string;
612
+ }
613
+
614
+ export type TFlexProjectManagementRecord =
615
+ | IFlexProjectManagementSnapshot
616
+ | IFlexProjectManagementTombstone;
617
+
618
+ export type TFlexProjectManagementWriteActor = 'agent' | 'application';
619
+
620
+ export interface IFlexProjectManagementWriteContext {
621
+ actor: TFlexProjectManagementWriteActor;
622
+ runId?: string;
623
+ agent?: string;
624
+ toolCallId?: string;
625
+ }
626
+
627
+ export interface IFlexProjectManagementStore {
628
+ load(
629
+ storageKey: string,
630
+ sessionId: string,
631
+ ): Promise<TFlexProjectManagementRecord | undefined>;
632
+ save(
633
+ storageKey: string,
634
+ sessionId: string,
635
+ snapshot: IFlexProjectManagementSnapshot,
636
+ expectedRevision: number,
637
+ writeContext: IFlexProjectManagementWriteContext,
638
+ ): Promise<void>;
639
+ tombstoneSession(
640
+ storageKey: string,
641
+ sessionId: string,
642
+ tombstone: IFlexProjectManagementTombstone,
643
+ expectedRevision: number,
644
+ ): Promise<void>;
645
+ purgeNamespace(storageKey: string): Promise<void>;
646
+ }
647
+
648
+ export interface IFlexCreateProjectTaskInput {
649
+ id: string;
650
+ content: string;
651
+ status?: TFlexProjectTaskStatus;
652
+ priority?: TFlexProjectTaskPriority;
653
+ }
654
+
655
+ export interface IFlexUpdateProjectTaskInput {
656
+ id: string;
657
+ content?: string;
658
+ status?: TFlexProjectTaskStatus;
659
+ priority?: TFlexProjectTaskPriority;
660
+ }
661
+
662
+ export interface IFlexProjectStateResult {
663
+ revision: number;
664
+ state: IFlexProjectManagementSnapshot;
665
+ }
666
+
667
+ export interface IFlexProjectTaskResult extends IFlexProjectStateResult {
668
+ task: IFlexProjectTask;
669
+ }
670
+
671
+ export interface IFlexProjectTasksResult extends IFlexProjectStateResult {
672
+ tasks: IFlexProjectTask[];
673
+ }
674
+
675
+ export interface IFlexProjectGoalResult extends IFlexProjectStateResult {
676
+ goal: string | null;
677
+ }
678
+
679
+ export interface IFlexProjectScratchpadResult extends IFlexProjectStateResult {
680
+ scratchpad: string;
681
+ }
682
+
567
683
  export interface IFlexScopeStore {
568
684
  load(storageKey: string): Promise<IFlexScopeSnapshot | undefined>;
569
685
  save(
@@ -617,6 +733,7 @@ export interface IFlexHarnessStores {
617
733
  scopes: IFlexScopeStore;
618
734
  projections: IFlexProjectionStore;
619
735
  permissions: IFlexPermissionStore;
736
+ projectManagement: IFlexProjectManagementStore;
620
737
  agentEvents: IFlexAgentEventStoreProvider;
621
738
  jobs: IFlexToolJobStoreProvider;
622
739
  }
@@ -875,6 +992,17 @@ export interface IFlexSubagentDefinition {
875
992
  maxSteps?: number;
876
993
  }
877
994
 
995
+ export interface IFlexProjectManagementBuiltInToolsOptions {
996
+ task?: boolean;
997
+ goal?: boolean;
998
+ scratchpad?: boolean;
999
+ }
1000
+
1001
+ export interface IFlexBuiltInToolsOptions {
1002
+ renameSession?: boolean;
1003
+ projectManagement?: IFlexProjectManagementBuiltInToolsOptions;
1004
+ }
1005
+
878
1006
  export interface IFlexHarnessOptions<TScope> {
879
1007
  scopeResolver: IFlexScopeResolver<TScope>;
880
1008
  modelResolver: IFlexModelResolver<TScope>;
@@ -891,6 +1019,7 @@ export interface IFlexHarnessOptions<TScope> {
891
1019
  reversionPolicy?: TFlexReversionPolicy;
892
1020
  externalErrorProjector?: TFlexExternalErrorProjector;
893
1021
  subagents?: IFlexSubagentDefinition[];
1022
+ builtInTools?: IFlexBuiltInToolsOptions;
894
1023
  slashCommands?: readonly TFlexSlashCommandRegistration<TScope>[];
895
1024
  maxSubagentDepth?: number;
896
1025
  maxSubagentCallsPerRun?: number;
package/ts/utils.json.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  FLEX_REVERSION_REFERENCE_MAX_BYTES,
7
7
  FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES,
8
8
  FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES,
9
+ FLEX_SESSION_GENERATION_ID_MAX_BYTES,
9
10
  } from './interfaces.js';
10
11
  import type {
11
12
  IFlexJsonLimits,
@@ -432,6 +433,39 @@ const activityStatuses = [
432
433
  'cancelled',
433
434
  ] as const;
434
435
 
436
+ function validateOptionalSessionGeneration(
437
+ value: Record<string, unknown>,
438
+ path: string,
439
+ ): { sessionGenerationId?: string; sessionGenerationSequence?: number } {
440
+ const hasId = value.sessionGenerationId !== undefined;
441
+ const hasSequence = value.sessionGenerationSequence !== undefined;
442
+ if (hasId !== hasSequence) {
443
+ throw new FlexHarnessStoreFormatError(`${path} has partial session generation fields.`);
444
+ }
445
+ if (!hasId) return {};
446
+ const sessionGenerationId = requireString(
447
+ value.sessionGenerationId,
448
+ `${path}.sessionGenerationId`,
449
+ );
450
+ if (Buffer.byteLength(sessionGenerationId, 'utf8') > FLEX_SESSION_GENERATION_ID_MAX_BYTES) {
451
+ throw new FlexHarnessStoreFormatError(
452
+ `${path}.sessionGenerationId exceeds ${FLEX_SESSION_GENERATION_ID_MAX_BYTES} UTF-8 bytes.`,
453
+ );
454
+ }
455
+ if (
456
+ !Number.isSafeInteger(value.sessionGenerationSequence)
457
+ || Number(value.sessionGenerationSequence) < 1
458
+ ) {
459
+ throw new FlexHarnessStoreFormatError(
460
+ `${path}.sessionGenerationSequence must be a positive integer.`,
461
+ );
462
+ }
463
+ return {
464
+ sessionGenerationId,
465
+ sessionGenerationSequence: value.sessionGenerationSequence as number,
466
+ };
467
+ }
468
+
435
469
  function validateSession(value: unknown, path: string): IValidatedSessionIdentity {
436
470
  const session = requireRecord(value, path);
437
471
  requireOnlyKeys(
@@ -439,6 +473,8 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
439
473
  [
440
474
  'scopeId',
441
475
  'sessionId',
476
+ 'sessionGenerationId',
477
+ 'sessionGenerationSequence',
442
478
  'title',
443
479
  'createdAt',
444
480
  'updatedAt',
@@ -455,6 +491,7 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
455
491
  );
456
492
  requireString(session.scopeId, `${path}.scopeId`);
457
493
  const sessionId = requireString(session.sessionId, `${path}.sessionId`);
494
+ const generation = validateOptionalSessionGeneration(session, path);
458
495
  requireOptionalString(session.title, `${path}.title`);
459
496
  requireString(session.createdAt, `${path}.createdAt`);
460
497
  requireString(session.updatedAt, `${path}.updatedAt`);
@@ -496,6 +533,7 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
496
533
  }
497
534
  return {
498
535
  sessionId,
536
+ ...generation,
499
537
  ...(relationshipCount === 0
500
538
  ? {}
501
539
  : {
@@ -541,6 +579,8 @@ interface IValidatedMessageIdentity {
541
579
 
542
580
  interface IValidatedSessionIdentity {
543
581
  sessionId: string;
582
+ sessionGenerationId?: string;
583
+ sessionGenerationSequence?: number;
544
584
  parentSessionId?: string;
545
585
  parentRunId?: string;
546
586
  parentToolCallId?: string;
@@ -780,10 +820,19 @@ export function assertFlexScopeSnapshot(value: unknown): asserts value is IFlexS
780
820
  const tombstone = requireRecord(snapshot.tombstones[index], path);
781
821
  requireOnlyKeys(
782
822
  tombstone,
783
- ['sessionId', 'deletedAt', 'rootSessionId', 'depth', 'parentSessionId'],
823
+ [
824
+ 'sessionId',
825
+ 'sessionGenerationId',
826
+ 'sessionGenerationSequence',
827
+ 'deletedAt',
828
+ 'rootSessionId',
829
+ 'depth',
830
+ 'parentSessionId',
831
+ ],
784
832
  path,
785
833
  );
786
834
  const sessionId = requireString(tombstone.sessionId, `${path}.sessionId`);
835
+ validateOptionalSessionGeneration(tombstone, path);
787
836
  requireString(tombstone.deletedAt, `${path}.deletedAt`);
788
837
  const parentSessionId = tombstone.parentSessionId === undefined
789
838
  ? undefined