@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.core.d.ts +4 -0
- package/dist_ts/classes.flexharness.core.js +69 -25
- package/dist_ts/classes.flexharness.imports.js +4 -3
- package/dist_ts/classes.flexharness.reversions.d.ts +7 -1
- package/dist_ts/classes.flexharness.reversions.js +43 -29
- package/dist_ts/classes.flexharness.sessions.js +2 -6
- package/dist_ts/classes.stores.js +20 -18
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.d.ts +24 -2
- package/dist_ts/interfaces.flexharness.d.ts +8 -1
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +3 -3
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.projectionchange.d.ts +34 -0
- package/dist_ts/utils.projectionchange.js +135 -0
- package/dist_ts/utils.storecodecs.d.ts +10 -3
- package/dist_ts/utils.storecodecs.js +29 -4
- package/dist_ts/utils.storerules.d.ts +1 -1
- package/dist_ts/utils.storerules.js +1 -1
- package/dist_ts_migration/v3_legacyflexharness.js +6 -5
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +5 -4
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +44 -25
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +152 -20
- package/dist_ts_stores_nosqldb/classes.recordstore.js +369 -66
- package/dist_ts_stores_nosqldb/index.d.ts +1 -1
- package/dist_ts_stores_nosqldb/index.js +2 -2
- package/dist_ts_stores_nosqldb/plugins.d.ts +1 -1
- package/dist_ts_stores_nosqldb/plugins.js +1 -1
- package/dist_ts_stores_testing/contract.d.ts +33 -0
- package/dist_ts_stores_testing/contract.js +566 -0
- package/dist_ts_stores_testing/fixtures.d.ts +20 -0
- package/dist_ts_stores_testing/fixtures.js +112 -0
- package/dist_ts_stores_testing/index.d.ts +2 -0
- package/dist_ts_stores_testing/index.js +3 -0
- package/dist_ts_stores_testing/plugins.d.ts +5 -0
- package/dist_ts_stores_testing/plugins.js +7 -0
- package/dist_ts_typedrequest/catalog.d.ts +4 -4
- package/dist_ts_typedrequest/catalog.js +25 -7
- package/dist_ts_typedrequest/describe.js +23 -11
- package/dist_ts_typedrequest/interfaces.d.ts +17 -1
- package/package.json +9 -3
- package/readme.md +6 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.core.ts +74 -24
- package/ts/classes.flexharness.imports.ts +3 -2
- package/ts/classes.flexharness.reversions.ts +53 -28
- package/ts/classes.flexharness.sessions.ts +2 -5
- package/ts/classes.stores.ts +40 -37
- package/ts/index.ts +5 -0
- package/ts/interfaces.flexharness.ts +8 -1
- package/ts/interfaces.ts +27 -2
- package/ts/plugins.ts +12 -7
- package/ts/readme.md +6 -3
- package/ts/utils.projectionchange.ts +190 -0
- package/ts/utils.storecodecs.ts +42 -7
- package/ts/utils.storerules.ts +2 -2
- package/ts_migration/v3_legacyflexharness.ts +11 -6
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +59 -46
- package/ts_stores_nosqldb/classes.recordstore.ts +445 -64
- package/ts_stores_nosqldb/index.ts +1 -0
- package/ts_stores_nosqldb/plugins.ts +4 -3
- package/ts_stores_nosqldb/readme.md +6 -2
- package/ts_stores_testing/contract.ts +776 -0
- package/ts_stores_testing/fixtures.ts +127 -0
- package/ts_stores_testing/index.ts +17 -0
- package/ts_stores_testing/plugins.ts +20 -0
- package/ts_stores_testing/readme.md +62 -0
- package/ts_typedrequest/catalog.ts +24 -6
- package/ts_typedrequest/describe.ts +19 -10
- package/ts_typedrequest/interfaces.ts +21 -1
- 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.
|
|
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
|
-
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
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')
|
|
982
|
-
else if (outcome === 'rejected')
|
|
983
|
-
else if (outcome === 'interrupted' ||
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1447
|
-
|
|
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
|
-
|
|
406
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
: '
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
1086
|
-
if (
|
|
1087
|
-
const wasCompleted =
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
.
|
|
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
|
|
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
|
-
|
|
1274
|
-
|
|
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(
|
|
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
|
-
|
|
460
|
-
|
|
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) {
|
package/ts/classes.stores.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
IFlexProjectManagementStore,
|
|
15
15
|
IFlexProjectManagementTombstone,
|
|
16
16
|
IFlexProjectManagementWriteContext,
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
+
change: IFlexProjectionChange,
|
|
99
104
|
expectedRevision: number,
|
|
100
105
|
): Promise<void> {
|
|
101
106
|
const key = providerKey(storageKey, sessionId);
|
|
102
|
-
const validated =
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
207
|
-
|
|
208
|
-
private
|
|
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.
|
|
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
|
-
|
|
229
|
+
change: plugins.IAgentEventChange,
|
|
229
230
|
expectedRevision: number,
|
|
230
231
|
): Promise<number> {
|
|
231
232
|
this.assertSession(sessionId);
|
|
232
233
|
validateExpectedRevision(expectedRevision);
|
|
233
|
-
const
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
840
|
+
change: IFlexProjectionChange,
|
|
836
841
|
expectedRevision: number,
|
|
837
842
|
): Promise<void> {
|
|
838
|
-
const validated =
|
|
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
|
-
|
|
1070
|
-
|
|
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.
|
|
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
|
-
|
|
1100
|
+
change: plugins.IAgentEventChange,
|
|
1101
1101
|
expectedRevision: number,
|
|
1102
1102
|
): Promise<number> {
|
|
1103
1103
|
this.assertSession(sessionId);
|
|
1104
1104
|
validateExpectedRevision(expectedRevision);
|
|
1105
|
-
const
|
|
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.
|
|
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.
|
|
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.
|
|
89
|
+
eventStore: plugins.IAgentEventStore;
|
|
83
90
|
agentSession: plugins.IAgentSession;
|
|
84
91
|
agentSessionAbortCompleted: boolean;
|
|
85
92
|
agentSessionCloseCompleted: boolean;
|