@modelprofile.com/flexharness 11.3.1 → 12.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.
Files changed (73) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.flexharness.core.d.ts +4 -0
  3. package/dist_ts/classes.flexharness.core.js +69 -25
  4. package/dist_ts/classes.flexharness.imports.js +4 -3
  5. package/dist_ts/classes.flexharness.reversions.d.ts +7 -1
  6. package/dist_ts/classes.flexharness.reversions.js +43 -29
  7. package/dist_ts/classes.flexharness.sessions.js +2 -6
  8. package/dist_ts/classes.stores.js +20 -18
  9. package/dist_ts/index.d.ts +1 -0
  10. package/dist_ts/index.js +2 -1
  11. package/dist_ts/interfaces.d.ts +24 -2
  12. package/dist_ts/interfaces.flexharness.d.ts +8 -1
  13. package/dist_ts/interfaces.js +1 -1
  14. package/dist_ts/plugins.d.ts +3 -3
  15. package/dist_ts/plugins.js +3 -3
  16. package/dist_ts/utils.projectionchange.d.ts +34 -0
  17. package/dist_ts/utils.projectionchange.js +135 -0
  18. package/dist_ts/utils.storecodecs.d.ts +10 -3
  19. package/dist_ts/utils.storecodecs.js +29 -4
  20. package/dist_ts/utils.storerules.d.ts +1 -1
  21. package/dist_ts/utils.storerules.js +1 -1
  22. package/dist_ts_migration/v3_legacyflexharness.js +6 -5
  23. package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +5 -4
  24. package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +44 -25
  25. package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +152 -20
  26. package/dist_ts_stores_nosqldb/classes.recordstore.js +369 -66
  27. package/dist_ts_stores_nosqldb/index.d.ts +1 -1
  28. package/dist_ts_stores_nosqldb/index.js +2 -2
  29. package/dist_ts_stores_nosqldb/plugins.d.ts +1 -1
  30. package/dist_ts_stores_nosqldb/plugins.js +1 -1
  31. package/dist_ts_stores_testing/contract.d.ts +33 -0
  32. package/dist_ts_stores_testing/contract.js +566 -0
  33. package/dist_ts_stores_testing/fixtures.d.ts +20 -0
  34. package/dist_ts_stores_testing/fixtures.js +112 -0
  35. package/dist_ts_stores_testing/index.d.ts +2 -0
  36. package/dist_ts_stores_testing/index.js +3 -0
  37. package/dist_ts_stores_testing/plugins.d.ts +5 -0
  38. package/dist_ts_stores_testing/plugins.js +7 -0
  39. package/dist_ts_typedrequest/catalog.d.ts +4 -4
  40. package/dist_ts_typedrequest/catalog.js +25 -7
  41. package/dist_ts_typedrequest/describe.js +23 -11
  42. package/dist_ts_typedrequest/interfaces.d.ts +17 -1
  43. package/package.json +9 -3
  44. package/readme.md +6 -3
  45. package/ts/00_commitinfo_data.ts +1 -1
  46. package/ts/classes.flexharness.core.ts +74 -24
  47. package/ts/classes.flexharness.imports.ts +3 -2
  48. package/ts/classes.flexharness.reversions.ts +53 -28
  49. package/ts/classes.flexharness.sessions.ts +2 -5
  50. package/ts/classes.stores.ts +40 -37
  51. package/ts/index.ts +5 -0
  52. package/ts/interfaces.flexharness.ts +8 -1
  53. package/ts/interfaces.ts +27 -2
  54. package/ts/plugins.ts +12 -7
  55. package/ts/readme.md +6 -3
  56. package/ts/utils.projectionchange.ts +190 -0
  57. package/ts/utils.storecodecs.ts +42 -7
  58. package/ts/utils.storerules.ts +2 -2
  59. package/ts_migration/v3_legacyflexharness.ts +11 -6
  60. package/ts_stores_nosqldb/classes.nosqldbstores.ts +59 -46
  61. package/ts_stores_nosqldb/classes.recordstore.ts +445 -64
  62. package/ts_stores_nosqldb/index.ts +1 -0
  63. package/ts_stores_nosqldb/plugins.ts +4 -3
  64. package/ts_stores_nosqldb/readme.md +6 -2
  65. package/ts_stores_testing/contract.ts +776 -0
  66. package/ts_stores_testing/fixtures.ts +127 -0
  67. package/ts_stores_testing/index.ts +17 -0
  68. package/ts_stores_testing/plugins.ts +20 -0
  69. package/ts_stores_testing/readme.md +62 -0
  70. package/ts_typedrequest/catalog.ts +24 -6
  71. package/ts_typedrequest/describe.ts +19 -10
  72. package/ts_typedrequest/interfaces.ts +21 -1
  73. package/ts_typedrequest/readme.md +5 -3
@@ -21,6 +21,7 @@ import type {
21
21
  IFlexMessage,
22
22
  IFlexPartSnapshotEvent,
23
23
  IFlexPromptQueueLimits,
24
+ IFlexProjectionChange,
24
25
  IFlexProjectionSnapshotCurrent,
25
26
  IFlexReversionLimits,
26
27
  IFlexReversionSegment,
@@ -51,6 +52,7 @@ import {
51
52
  resolveJsonLimits,
52
53
  } from './utils.json.js';
53
54
  import { hydrateAgentMessages } from './utils.prompt.js';
55
+ import { createProjectionChange, freezeProjectionEntries } from './utils.projectionchange.js';
54
56
  import {
55
57
  normalizeAgentSessionPolicy,
56
58
  normalizeBuiltInTools,
@@ -550,7 +552,7 @@ export class FlexHarnessCore<TScope> {
550
552
  rememberedPermissionKeys: new Set(),
551
553
  permissionRevision: 0,
552
554
  permissionQueue: Promise.resolve(),
553
- eventStore: undefined as unknown as plugins.IAgentEventStoreV2,
555
+ eventStore: undefined as unknown as plugins.IAgentEventStore,
554
556
  agentSession: unavailable,
555
557
  agentSessionAbortCompleted: true,
556
558
  agentSessionCloseCompleted: true,
@@ -699,6 +701,8 @@ export class FlexHarnessCore<TScope> {
699
701
  terminalPromptQueueEntries: new Map(),
700
702
  outstandingPromptBytes: 0,
701
703
  };
704
+ freezeProjectionEntries(stored);
705
+ if (currentProjection) this.rememberSavedProjectionLists(stored);
702
706
  if (currentProjection?.metrics === undefined
703
707
  || stored.metrics.pendingGenerationId !== undefined
704
708
  || stored.metrics.pendingCompactionId !== undefined
@@ -971,18 +975,20 @@ export class FlexHarnessCore<TScope> {
971
975
  if (stored.stagedTerminals.length > 0) stored.stagedTerminals = [];
972
976
  const activeEvents = stored.agentSession.getEvents();
973
977
  const activeEventIds = new Set(activeEvents.map((event) => event.id));
974
- for (let index = 0; index < stored.reversionSegments.length; index++) {
975
- const segment = stored.reversionSegments[index];
976
- if (segment.eventIds.length > 0 && !segment.eventIds.some((id) => activeEventIds.has(id))) {
977
- segment.contextAvailable = false;
978
- }
979
- if (segment.status !== 'capturing') {
978
+ stored.reversionSegments = stored.reversionSegments.map((segment) => {
979
+ const contextAvailable = segment.contextAvailable
980
+ && (segment.eventIds.length === 0 || segment.eventIds.some((id) => activeEventIds.has(id)));
981
+ let status = segment.status;
982
+ if (status !== 'capturing') {
980
983
  const outcome = outcomes.get(segment.runId);
981
- if (outcome === 'accepted') segment.status = 'completed';
982
- else if (outcome === 'rejected') segment.status = 'failed';
983
- else if (outcome === 'interrupted' || segment.contextAvailable) segment.status = 'cancelled';
984
+ if (outcome === 'accepted') status = 'completed';
985
+ else if (outcome === 'rejected') status = 'failed';
986
+ else if (outcome === 'interrupted' || contextAvailable) status = 'cancelled';
984
987
  }
985
- }
988
+ return contextAvailable === segment.contextAvailable && status === segment.status
989
+ ? segment
990
+ : { ...segment, contextAvailable, status };
991
+ });
986
992
  stored.revertCursor = stored.reversionSegments
987
993
  .slice(0, reversionVisibilityBoundary)
988
994
  .filter((segment) => segment.status === 'completed')
@@ -1032,17 +1038,19 @@ export class FlexHarnessCore<TScope> {
1032
1038
  });
1033
1039
  if (projectionChanged) {
1034
1040
  const expected = stored.projectionRevision;
1035
- const snapshot = this.createProjectionSnapshot(stored, expected + 1);
1041
+ freezeProjectionEntries(stored);
1042
+ const change = this.createProjectionChange(stored, expected + 1);
1036
1043
  await this.stores.projections.save(
1037
1044
  storageKey,
1038
1045
  stored.session.sessionId,
1039
- snapshot,
1046
+ change,
1040
1047
  expected,
1041
1048
  );
1042
1049
  stored.projectionSchemaVersion = 3;
1043
1050
  delete stored.projectionBaseline;
1051
+ this.rememberSavedProjectionLists(stored);
1044
1052
  stored.projectionReconciliationRequired = false;
1045
- stored.projectionRevision = snapshot.revision;
1053
+ stored.projectionRevision = change.revision;
1046
1054
  }
1047
1055
  return beforeSession !== JSON.stringify(stored.session);
1048
1056
  }
@@ -1239,9 +1247,10 @@ export class FlexHarnessCore<TScope> {
1239
1247
  ? undefined
1240
1248
  : cloneSerializable(stored.projectionBaseline);
1241
1249
  const beforeMetrics = cloneSerializable(stored.metrics);
1242
- const beforeMessages = cloneSerializable(stored.messages);
1250
+ // List entries are frozen and replaced rather than edited, so copies of the lists suffice.
1251
+ const beforeMessages = stored.messages.slice();
1243
1252
  const beforeStages = cloneSerializable(stored.stagedTerminals);
1244
- const beforeSegments = cloneSerializable(stored.reversionSegments);
1253
+ const beforeSegments = stored.reversionSegments.slice();
1245
1254
  const beforeCursor = stored.revertCursor;
1246
1255
  const beforeExcludedRunIds = cloneSerializable(stored.excludedRunIds);
1247
1256
  const beforePendingReversion = stored.pendingReversion === undefined
@@ -1285,18 +1294,20 @@ export class FlexHarnessCore<TScope> {
1285
1294
  restore();
1286
1295
  throw error;
1287
1296
  }
1288
- const snapshot = this.createProjectionSnapshot(stored, beforeRevision + 1);
1297
+ freezeProjectionEntries(stored);
1298
+ const revision = beforeRevision + 1;
1289
1299
  try {
1290
1300
  await this.stores.projections.save(
1291
1301
  state.storageKey,
1292
1302
  stored.session.sessionId,
1293
- snapshot,
1303
+ this.createProjectionChange(stored, revision),
1294
1304
  beforeRevision,
1295
1305
  );
1296
1306
  stored.projectionSchemaVersion = 3;
1297
1307
  delete stored.projectionBaseline;
1308
+ this.rememberSavedProjectionLists(stored);
1298
1309
  stored.projectionReconciliationRequired = false;
1299
- stored.projectionRevision = snapshot.revision;
1310
+ stored.projectionRevision = revision;
1300
1311
  return result;
1301
1312
  } catch (error) {
1302
1313
  if (error instanceof FlexHarnessStoreConflictError) {
@@ -1305,11 +1316,13 @@ export class FlexHarnessCore<TScope> {
1305
1316
  }
1306
1317
  if (error instanceof FlexHarnessStoreCommitUncertainError) {
1307
1318
  stored.projectionSchemaVersion = 3;
1319
+ delete stored.savedProjectionLists;
1308
1320
  stored.projectionReconciliationRequired = true;
1309
- stored.projectionRevision = snapshot.revision;
1321
+ stored.projectionRevision = revision;
1310
1322
  state.lifecycle = 'fenced';
1311
1323
  throw error;
1312
1324
  }
1325
+ const snapshot = this.createProjectionSnapshot(stored, revision);
1313
1326
  let current: TFlexProjectionSnapshot | undefined;
1314
1327
  let reconciliationError: unknown;
1315
1328
  try {
@@ -1323,8 +1336,9 @@ export class FlexHarnessCore<TScope> {
1323
1336
  if (current && JSON.stringify(current) === JSON.stringify(snapshot)) {
1324
1337
  stored.projectionSchemaVersion = 3;
1325
1338
  delete stored.projectionBaseline;
1339
+ this.rememberSavedProjectionLists(stored);
1326
1340
  stored.projectionReconciliationRequired = false;
1327
- stored.projectionRevision = snapshot.revision;
1341
+ stored.projectionRevision = revision;
1328
1342
  throw error;
1329
1343
  }
1330
1344
  if (
@@ -1335,8 +1349,9 @@ export class FlexHarnessCore<TScope> {
1335
1349
  throw error;
1336
1350
  }
1337
1351
  stored.projectionSchemaVersion = 3;
1352
+ delete stored.savedProjectionLists;
1338
1353
  stored.projectionReconciliationRequired = true;
1339
- stored.projectionRevision = snapshot.revision;
1354
+ stored.projectionRevision = revision;
1340
1355
  state.lifecycle = 'fenced';
1341
1356
  throw reconciliationError === undefined
1342
1357
  ? error
@@ -1380,6 +1395,35 @@ export class FlexHarnessCore<TScope> {
1380
1395
  };
1381
1396
  }
1382
1397
 
1398
+ /** The change from the lists the store holds to the session's projection at `revision`. */
1399
+ private createProjectionChange(
1400
+ stored: IStoredSessionState,
1401
+ revision: number,
1402
+ ): IFlexProjectionChange {
1403
+ return createProjectionChange(stored.savedProjectionLists, {
1404
+ schemaVersion: 3,
1405
+ revision,
1406
+ metrics: cloneSerializable(stored.metrics),
1407
+ messages: stored.messages,
1408
+ stagedTerminals: cloneSerializable(stored.stagedTerminals),
1409
+ reversionSegments: stored.reversionSegments,
1410
+ revertCursor: stored.revertCursor,
1411
+ excludedRunIds: cloneSerializable(stored.excludedRunIds),
1412
+ ...(stored.pendingReversion === undefined
1413
+ ? {}
1414
+ : { pendingReversion: cloneSerializable(stored.pendingReversion) }),
1415
+ pendingReversionReleases: cloneSerializable(stored.pendingReversionReleases),
1416
+ });
1417
+ }
1418
+
1419
+ /** Records the session's lists as the ones the store now holds. */
1420
+ private rememberSavedProjectionLists(stored: IStoredSessionState): void {
1421
+ stored.savedProjectionLists = {
1422
+ messages: stored.messages.slice(),
1423
+ reversionSegments: stored.reversionSegments.slice(),
1424
+ };
1425
+ }
1426
+
1383
1427
  public upgradeProjectionSnapshot(
1384
1428
  projection: TFlexProjectionSnapshot,
1385
1429
  ): IFlexProjectionSnapshotCurrent {
@@ -1443,8 +1487,14 @@ export class FlexHarnessCore<TScope> {
1443
1487
  if (current.pendingReversion === undefined) delete stored.pendingReversion;
1444
1488
  else stored.pendingReversion = current.pendingReversion;
1445
1489
  stored.pendingReversionReleases = current.pendingReversionReleases;
1446
- if (projection.schemaVersion === 3) delete stored.projectionBaseline;
1447
- else stored.projectionBaseline = cloneSerializable(projection);
1490
+ freezeProjectionEntries(stored);
1491
+ if (projection.schemaVersion === 3) {
1492
+ delete stored.projectionBaseline;
1493
+ this.rememberSavedProjectionLists(stored);
1494
+ } else {
1495
+ stored.projectionBaseline = cloneSerializable(projection);
1496
+ delete stored.savedProjectionLists;
1497
+ }
1448
1498
  stored.projectionSchemaVersion = projection.schemaVersion;
1449
1499
  stored.projectionRevision = projection.revision;
1450
1500
  stored.projectionReconciliationRequired = false;
@@ -17,6 +17,7 @@ import {
17
17
  type TFlexBeginSessionImportResult,
18
18
  } from './interfaces.js';
19
19
  import { cloneSerializable } from './utils.json.js';
20
+ import { createWholeProjectionChange } from './utils.projectionchange.js';
20
21
  import { publicSnapshot } from './utils.transfer.js';
21
22
  import {
22
23
  normalizeBeginSessionImportOptions,
@@ -231,7 +232,7 @@ export class FlexHarnessSessionImports<TScope> {
231
232
  });
232
233
  });
233
234
  reserved = true;
234
- await this.core.stores.projections.save(state.storageKey, sessionId, {
235
+ await this.core.stores.projections.save(state.storageKey, sessionId, createWholeProjectionChange({
235
236
  schemaVersion: 3,
236
237
  revision: 1,
237
238
  metrics: createSessionMetricsState(false),
@@ -241,7 +242,7 @@ export class FlexHarnessSessionImports<TScope> {
241
242
  revertCursor: 0,
242
243
  excludedRunIds: [],
243
244
  pendingReversionReleases: [],
244
- }, 0);
245
+ }), 0);
245
246
  const runtime = await this.core.loadSessionRuntime(state, metadata, scopeId, scope);
246
247
  loaded = runtime;
247
248
  await this.core.mutateScope(state, () => {
@@ -28,6 +28,7 @@ import {
28
28
  type TJsonValue,
29
29
  } from './interfaces.js';
30
30
  import { cloneSerializable, normalizeJsonValue } from './utils.json.js';
31
+ import { createWholeProjectionChange } from './utils.projectionchange.js';
31
32
  import { publicSnapshot } from './utils.transfer.js';
32
33
  import type {
33
34
  TCanonicalOutcome,
@@ -402,13 +403,8 @@ export class FlexHarnessReversions<TScope> {
402
403
  && !segment.eventIds.some((eventId) => activeEvents.has(eventId)))) return false;
403
404
  let branchCommitted = false;
404
405
  await this.core.mutateProjection(state, stored, () => {
405
- for (const segment of stored.reversionSegments) {
406
- if (
407
- segment.contextAvailable
408
- && segment.eventIds.length > 0
409
- && !segment.eventIds.some((eventId) => activeEvents.has(eventId))
410
- ) segment.contextAvailable = false;
411
- }
406
+ this.markReversionContextUnavailable(stored, (segment) =>
407
+ segment.eventIds.length > 0 && !segment.eventIds.some((eventId) => activeEvents.has(eventId)));
412
408
  branchCommitted = this.commitArchivedHiddenBranchState(stored);
413
409
  this.pruneReversionState(stored, false, true);
414
410
  }, true);
@@ -618,16 +614,22 @@ export class FlexHarnessReversions<TScope> {
618
614
  return;
619
615
  }
620
616
  await this.core.mutateProjection(state, stored, () => {
621
- segment.status = outcome === 'accepted'
622
- ? 'completed'
623
- : outcome === 'rejected'
624
- ? 'failed'
625
- : 'cancelled';
626
- this.applyFinalizedReversionOutcome(stored, segment, finalizedOutcome);
627
- segment.eventIds = stored.agentSession.getEvents()
617
+ const index = stored.reversionSegments.findIndex((entry) => entry.runId === pending.runId);
618
+ if (index < 0) throw new FlexHarnessValidationError('Pending capture references a missing segment.');
619
+ const finalized: IFlexReversionSegment = {
620
+ ...stored.reversionSegments[index],
621
+ status: outcome === 'accepted'
622
+ ? 'completed'
623
+ : outcome === 'rejected'
624
+ ? 'failed'
625
+ : 'cancelled',
626
+ };
627
+ this.applyFinalizedReversionOutcome(stored, finalized, finalizedOutcome);
628
+ finalized.eventIds = stored.agentSession.getEvents()
628
629
  .filter((event) => event.generationId === pending.runId)
629
630
  .map((event) => event.id);
630
- if (segment.status === 'completed') stored.revertCursor++;
631
+ stored.reversionSegments[index] = finalized;
632
+ if (finalized.status === 'completed') stored.revertCursor++;
631
633
  if (outcome === undefined && !stored.excludedRunIds.includes(pending.runId)) {
632
634
  stored.excludedRunIds.push(pending.runId);
633
635
  }
@@ -1077,23 +1079,42 @@ export class FlexHarnessReversions<TScope> {
1077
1079
  };
1078
1080
  }
1079
1081
 
1080
- /** Records a run's terminal status and events on its segment and moves the revert cursor. */
1082
+ /**
1083
+ * Replaces a run's segment with a copy that records its terminal status and events, moves the
1084
+ * revert cursor and returns the copy.
1085
+ */
1081
1086
  private closeRunReversionSegment(
1082
1087
  run: IActiveRun,
1083
1088
  status: 'completed' | 'failed' | 'cancelled',
1084
1089
  ): IFlexReversionSegment {
1085
- const segment = run.stored.reversionSegments.find((entry) => entry.runId === run.runId);
1086
- if (!segment) throw new FlexHarnessValidationError('Reversion segment changed before finalization.');
1087
- const wasCompleted = segment.status === 'completed';
1088
- segment.status = status;
1089
- segment.eventIds = run.stored.agentSession.getEvents()
1090
- .filter((event) => event.generationId === run.runId)
1091
- .map((event) => event.id);
1090
+ const index = run.stored.reversionSegments.findIndex((entry) => entry.runId === run.runId);
1091
+ if (index < 0) throw new FlexHarnessValidationError('Reversion segment changed before finalization.');
1092
+ const wasCompleted = run.stored.reversionSegments[index].status === 'completed';
1093
+ const closed: IFlexReversionSegment = {
1094
+ ...run.stored.reversionSegments[index],
1095
+ status,
1096
+ eventIds: run.stored.agentSession.getEvents()
1097
+ .filter((event) => event.generationId === run.runId)
1098
+ .map((event) => event.id),
1099
+ };
1100
+ run.stored.reversionSegments[index] = closed;
1092
1101
  if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
1093
1102
  else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
1094
- return segment;
1103
+ return closed;
1095
1104
  }
1096
1105
 
1106
+ /** Replaces the segments `unavailable` selects with copies that have no context available. */
1107
+ public markReversionContextUnavailable(
1108
+ stored: IStoredSessionState,
1109
+ unavailable: (segment: IFlexReversionSegment) => boolean,
1110
+ ): void {
1111
+ stored.reversionSegments = stored.reversionSegments.map((segment) =>
1112
+ segment.contextAvailable && unavailable(segment)
1113
+ ? { ...segment, contextAvailable: false }
1114
+ : segment);
1115
+ }
1116
+
1117
+ /** Records a finalized capture on `segment`, an unsaved copy the caller puts in place. */
1097
1118
  private applyFinalizedReversionOutcome(
1098
1119
  stored: IStoredSessionState,
1099
1120
  segment: IFlexReversionSegment,
@@ -1270,9 +1291,8 @@ export class FlexHarnessReversions<TScope> {
1270
1291
  const archivedIds = new Set(events.flatMap((event) => event.type === 'context-compaction' && event.automatic
1271
1292
  ? event.coveredEventIds.filter((id) => !activeIds.has(id)) : []));
1272
1293
  if (archivedIds.size === 0) return;
1273
- for (const segment of stored.reversionSegments) {
1274
- if (segment.eventIds.some((id) => archivedIds.has(id))) segment.contextAvailable = false;
1275
- }
1294
+ this.markReversionContextUnavailable(stored, (segment) =>
1295
+ segment.eventIds.some((id) => archivedIds.has(id)));
1276
1296
  this.commitArchivedHiddenBranchState(stored);
1277
1297
  this.pruneReversionState(stored, false, true);
1278
1298
  }
@@ -1394,7 +1414,12 @@ export class FlexHarnessReversions<TScope> {
1394
1414
  const persistDirect = async (next: IFlexProjectionSnapshotCurrent): Promise<void> => {
1395
1415
  const before = durableProjection;
1396
1416
  try {
1397
- await this.core.stores.projections.save(storageKey, sessionId, next, projection.revision);
1417
+ await this.core.stores.projections.save(
1418
+ storageKey,
1419
+ sessionId,
1420
+ createWholeProjectionChange(next),
1421
+ projection.revision,
1422
+ );
1398
1423
  durableProjection = next;
1399
1424
  projection = next;
1400
1425
  } catch (error) {
@@ -456,11 +456,8 @@ export class FlexHarnessSessions<TScope> {
456
456
  const archivedIds = new Set(archive.events.map((event) => event.id));
457
457
  await this.core.mutateProjection(state, stored, () => {
458
458
  branchCommitted = this.core.reversions.commitRevertedBranchState(stored);
459
- for (const segment of stored.reversionSegments) {
460
- if (segment.eventIds.some((eventId) => archivedIds.has(eventId))) {
461
- segment.contextAvailable = false;
462
- }
463
- }
459
+ this.core.reversions.markReversionContextUnavailable(stored, (segment) =>
460
+ segment.eventIds.some((eventId) => archivedIds.has(eventId)));
464
461
  this.core.reversions.pruneReversionState(stored);
465
462
  }, true);
466
463
  } catch (error) {
@@ -14,7 +14,7 @@ import type {
14
14
  IFlexProjectManagementStore,
15
15
  IFlexProjectManagementTombstone,
16
16
  IFlexProjectManagementWriteContext,
17
- IFlexProjectionSnapshotV3,
17
+ IFlexProjectionChange,
18
18
  IFlexProjectionStore,
19
19
  IFlexResultStore,
20
20
  IFlexScopeSnapshot,
@@ -36,10 +36,15 @@ import {
36
36
  assertFlexProjectManagementSnapshot,
37
37
  assertFlexProjectManagementTombstone,
38
38
  } from './utils.projectmanagement.js';
39
+ import {
40
+ createChangedProjectionSnapshot,
41
+ validateProjectionChangeSave,
42
+ } from './utils.projectionchange.js';
39
43
  import {
40
44
  assertFlexToolJobSnapshot,
41
- createAgentEventSnapshot,
45
+ createChangedAgentEventSnapshot,
42
46
  createToolJobSnapshot,
47
+ normalizeAgentEventChange,
43
48
  normalizeJsonObjectUndefined,
44
49
  validateAgentEventArchive,
45
50
  validateAgentEventSnapshot,
@@ -95,20 +100,17 @@ class InMemoryProjectionStore implements IFlexProjectionStore {
95
100
  public async save(
96
101
  storageKey: string,
97
102
  sessionId: string,
98
- snapshot: IFlexProjectionSnapshotV3,
103
+ change: IFlexProjectionChange,
99
104
  expectedRevision: number,
100
105
  ): Promise<void> {
101
106
  const key = providerKey(storageKey, sessionId);
102
- const validated = validateSnapshotSave(
103
- snapshot,
104
- expectedRevision,
105
- (value) => assertFlexProjectionSnapshot(value, sessionId),
106
- );
107
- const actualRevision = this.snapshots.get(key)?.revision ?? 0;
107
+ const validated = validateProjectionChangeSave(change, expectedRevision);
108
+ const current = this.snapshots.get(key);
109
+ const actualRevision = current?.revision ?? 0;
108
110
  if (actualRevision !== expectedRevision) {
109
111
  throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
110
112
  }
111
- this.snapshots.set(key, validated);
113
+ this.snapshots.set(key, createChangedProjectionSnapshot(current, validated, sessionId));
112
114
  }
113
115
 
114
116
  public async deleteSession(storageKey: string, sessionId: string): Promise<void> {
@@ -203,10 +205,9 @@ export class InMemoryFlexProjectManagementStore implements IFlexProjectManagemen
203
205
  }
204
206
  }
205
207
 
206
- class InMemoryAgentEventStore implements plugins.IAgentEventStoreV2 {
207
- public readonly eventSchemaVersion = 2 as const;
208
- private snapshot: plugins.IAgentEventSnapshotV2 | undefined;
209
- private readonly archives = new Map<string, plugins.IAgentEventArchiveV2>();
208
+ class InMemoryAgentEventStore implements plugins.IAgentEventStore {
209
+ private snapshot: plugins.IAgentEventSnapshot | undefined;
210
+ private readonly archives = new Map<string, plugins.IAgentEventArchive>();
210
211
 
211
212
  constructor(private readonly sessionId: string) {}
212
213
 
@@ -218,29 +219,32 @@ class InMemoryAgentEventStore implements plugins.IAgentEventStoreV2 {
218
219
  }
219
220
  }
220
221
 
221
- public async load(sessionId: string): Promise<plugins.IAgentEventSnapshotV2 | undefined> {
222
+ public async load(sessionId: string): Promise<plugins.IAgentEventSnapshot | undefined> {
222
223
  this.assertSession(sessionId);
223
224
  return this.snapshot ? cloneSerializable(this.snapshot) : undefined;
224
225
  }
225
226
 
226
227
  public async save(
227
228
  sessionId: string,
228
- events: readonly plugins.TAgentEvent[],
229
+ change: plugins.IAgentEventChange,
229
230
  expectedRevision: number,
230
231
  ): Promise<number> {
231
232
  this.assertSession(sessionId);
232
233
  validateExpectedRevision(expectedRevision);
233
- const snapshot = createAgentEventSnapshot(sessionId, events, expectedRevision + 1);
234
- validateAgentEventSnapshot(snapshot, sessionId);
234
+ const normalized = normalizeAgentEventChange(change);
235
235
  const actualRevision = this.snapshot?.revision ?? 0;
236
236
  if (actualRevision !== expectedRevision) {
237
237
  throw new plugins.AgentEventStoreConflictError(sessionId, expectedRevision, actualRevision);
238
238
  }
239
- this.snapshot = cloneSerializable(snapshot);
239
+ const snapshot = validateAgentEventSnapshot(
240
+ createChangedAgentEventSnapshot(sessionId, this.snapshot, normalized),
241
+ sessionId,
242
+ );
243
+ this.snapshot = snapshot;
240
244
  return snapshot.revision;
241
245
  }
242
246
 
243
- public async archive(archive: plugins.IAgentEventArchiveV2): Promise<void> {
247
+ public async archive(archive: plugins.IAgentEventArchive): Promise<void> {
244
248
  this.assertSession(archive.sessionId);
245
249
  const normalized = normalizeJsonObjectUndefined(archive, '$archive');
246
250
  const validated = validateAgentEventArchive(normalized, this.sessionId, archive.archiveId);
@@ -255,7 +259,7 @@ class InMemoryAgentEventStore implements plugins.IAgentEventStoreV2 {
255
259
  public async loadArchive(
256
260
  sessionId: string,
257
261
  archiveId: string,
258
- ): Promise<plugins.IAgentEventArchiveV2 | undefined> {
262
+ ): Promise<plugins.IAgentEventArchive | undefined> {
259
263
  this.assertSession(sessionId);
260
264
  validateIdentifier(archiveId, 'archiveId');
261
265
  const archive = this.archives.get(archiveId);
@@ -829,17 +833,14 @@ class JsonProjectionStore implements IFlexProjectionStore {
829
833
  });
830
834
  }
831
835
 
836
+ /** Applies the change and writes the whole projection again. */
832
837
  public async save(
833
838
  storageKey: string,
834
839
  sessionId: string,
835
- snapshot: IFlexProjectionSnapshotV3,
840
+ change: IFlexProjectionChange,
836
841
  expectedRevision: number,
837
842
  ): Promise<void> {
838
- const validated = validateSnapshotSave(
839
- snapshot,
840
- expectedRevision,
841
- (value) => assertFlexProjectionSnapshot(value, sessionId),
842
- );
843
+ const validated = validateProjectionChangeSave(change, expectedRevision);
843
844
  const filePath = this.root.sessionPath('projections', storageKey, sessionId);
844
845
  await JsonFileRoot.inQueue(filePath, async () => {
845
846
  const current = await this.root.read(filePath, (value) => {
@@ -850,7 +851,7 @@ class JsonProjectionStore implements IFlexProjectionStore {
850
851
  if (actualRevision !== expectedRevision) {
851
852
  throw new FlexHarnessStoreConflictError(providerKey(storageKey, sessionId), expectedRevision, actualRevision);
852
853
  }
853
- await this.root.write(filePath, validated);
854
+ await this.root.write(filePath, createChangedProjectionSnapshot(current, validated, sessionId));
854
855
  });
855
856
  }
856
857
 
@@ -1066,9 +1067,8 @@ async function clearJsonAgentEventSession(
1066
1067
  });
1067
1068
  }
1068
1069
 
1069
- class JsonAgentEventStore implements plugins.IAgentEventStoreV2 {
1070
- public readonly eventSchemaVersion = 2 as const;
1071
-
1070
+ /** Stores a session's event log in one file; a save rewrites the whole log. */
1071
+ class JsonAgentEventStore implements plugins.IAgentEventStore {
1072
1072
  constructor(
1073
1073
  private readonly root: JsonFileRoot,
1074
1074
  private readonly storageKey: string,
@@ -1083,7 +1083,7 @@ class JsonAgentEventStore implements plugins.IAgentEventStoreV2 {
1083
1083
  }
1084
1084
  }
1085
1085
 
1086
- public async load(sessionId: string): Promise<plugins.IAgentEventSnapshotV2 | undefined> {
1086
+ public async load(sessionId: string): Promise<plugins.IAgentEventSnapshot | undefined> {
1087
1087
  this.assertSession(sessionId);
1088
1088
  const filePath = this.root.sessionPath('events', this.storageKey, this.sessionId);
1089
1089
  return JsonFileRoot.inQueue(this.root.eventQueueKey(this.storageKey, this.sessionId), async () => {
@@ -1097,13 +1097,12 @@ class JsonAgentEventStore implements plugins.IAgentEventStoreV2 {
1097
1097
 
1098
1098
  public async save(
1099
1099
  sessionId: string,
1100
- events: readonly plugins.TAgentEvent[],
1100
+ change: plugins.IAgentEventChange,
1101
1101
  expectedRevision: number,
1102
1102
  ): Promise<number> {
1103
1103
  this.assertSession(sessionId);
1104
1104
  validateExpectedRevision(expectedRevision);
1105
- const snapshot = createAgentEventSnapshot(sessionId, events, expectedRevision + 1);
1106
- validateAgentEventSnapshot(snapshot, sessionId);
1105
+ const normalized = normalizeAgentEventChange(change);
1107
1106
  const filePath = this.root.sessionPath('events', this.storageKey, this.sessionId);
1108
1107
  return JsonFileRoot.inQueue(this.root.eventQueueKey(this.storageKey, this.sessionId), async () => {
1109
1108
  const current = await this.root.read(
@@ -1114,12 +1113,16 @@ class JsonAgentEventStore implements plugins.IAgentEventStoreV2 {
1114
1113
  if (actualRevision !== expectedRevision) {
1115
1114
  throw new plugins.AgentEventStoreConflictError(sessionId, expectedRevision, actualRevision);
1116
1115
  }
1116
+ const snapshot = validateAgentEventSnapshot(
1117
+ createChangedAgentEventSnapshot(sessionId, current, normalized),
1118
+ sessionId,
1119
+ );
1117
1120
  await this.root.write(filePath, snapshot);
1118
1121
  return snapshot.revision;
1119
1122
  });
1120
1123
  }
1121
1124
 
1122
- public async archive(archive: plugins.IAgentEventArchiveV2): Promise<void> {
1125
+ public async archive(archive: plugins.IAgentEventArchive): Promise<void> {
1123
1126
  this.assertSession(archive.sessionId);
1124
1127
  const normalized = normalizeJsonObjectUndefined(archive, '$archive');
1125
1128
  const validated = validateAgentEventArchive(normalized, this.sessionId, archive.archiveId);
@@ -1144,7 +1147,7 @@ class JsonAgentEventStore implements plugins.IAgentEventStoreV2 {
1144
1147
  public async loadArchive(
1145
1148
  sessionId: string,
1146
1149
  archiveId: string,
1147
- ): Promise<plugins.IAgentEventArchiveV2 | undefined> {
1150
+ ): Promise<plugins.IAgentEventArchive | undefined> {
1148
1151
  this.assertSession(sessionId);
1149
1152
  const filePath = this.root.archivePath(this.storageKey, this.sessionId, archiveId);
1150
1153
  return JsonFileRoot.inQueue(this.root.eventQueueKey(this.storageKey, this.sessionId), async () => {
package/ts/index.ts CHANGED
@@ -21,6 +21,11 @@ export {
21
21
  assertFlexProjectManagementTombstone,
22
22
  createEmptyFlexProjectManagementSnapshot,
23
23
  } from './utils.projectmanagement.js';
24
+ export {
25
+ applyFlexProjectionChange,
26
+ assertFlexProjectionChange,
27
+ createWholeProjectionChange,
28
+ } from './utils.projectionchange.js';
24
29
  export { assertFlexToolJobSnapshot } from './utils.storecodecs.js';
25
30
  export {
26
31
  FLEX_SLASH_COMMAND_MAX_INPUT_BYTES,
@@ -2,6 +2,7 @@ import type * as plugins from './plugins.js';
2
2
  import type { FlexHarnessAbortError } from './errors.js';
3
3
  import type { FlexToolCatalog } from './classes.toolcatalog.js';
4
4
  import type { FlexCodeSandbox } from './classes.codesandbox.js';
5
+ import type { IFlexProjectionLists } from './utils.projectionchange.js';
5
6
  import type {
6
7
  IFlexAgentContextInvocation,
7
8
  IFlexAffectedWorkspace,
@@ -69,6 +70,12 @@ export interface IStoredSessionState {
69
70
  pendingReversionReleases: IFlexPendingReversionRelease[];
70
71
  projectionSchemaVersion: 1 | 2 | 3;
71
72
  projectionBaseline?: TFlexProjectionSnapshot;
73
+ /**
74
+ * The lists as the store holds them, so a save sends only the entries that differ. Entries are
75
+ * frozen from their first save on and replaced rather than edited. Absent while the stored lists
76
+ * are unknown or of an older schema; the next save then sends whole lists.
77
+ */
78
+ savedProjectionLists?: IFlexProjectionLists;
72
79
  projectionReconciliationRequired: boolean;
73
80
  projectionRevision: number;
74
81
  projectionQueue: Promise<void>;
@@ -79,7 +86,7 @@ export interface IStoredSessionState {
79
86
  rememberedPermissionKeys: Set<string>;
80
87
  permissionRevision: number;
81
88
  permissionQueue: Promise<void>;
82
- eventStore: plugins.IAgentEventStoreV2;
89
+ eventStore: plugins.IAgentEventStore;
83
90
  agentSession: plugins.IAgentSession;
84
91
  agentSessionAbortCompleted: boolean;
85
92
  agentSessionCloseCompleted: boolean;