@modelprofile.com/flexharness 5.0.0 → 5.2.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/changelog.md +18 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +11 -1
- package/dist_ts/classes.flexharness.js +260 -143
- package/dist_ts/interfaces.d.ts +20 -7
- package/dist_ts/interfaces.js +2 -1
- package/dist_ts/utils.validation.d.ts +6 -1
- package/dist_ts/utils.validation.js +74 -2
- package/package.json +1 -1
- package/readme.md +40 -13
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +386 -130
- package/ts/interfaces.ts +24 -7
- package/ts/utils.validation.ts +125 -1
|
@@ -33,6 +33,8 @@ import type {
|
|
|
33
33
|
IFlexCallbackLimits,
|
|
34
34
|
IFlexCreateProjectTaskInput,
|
|
35
35
|
IFlexCreateSessionOptions,
|
|
36
|
+
IFlexDeleteSessionGenerationCohortInput,
|
|
37
|
+
IFlexDeleteSessionGenerationCohortResult,
|
|
36
38
|
IFlexErrorInfo,
|
|
37
39
|
IFlexEventArchiveMetadata,
|
|
38
40
|
IFlexExecutionContextHandle,
|
|
@@ -74,6 +76,8 @@ import type {
|
|
|
74
76
|
IFlexSchedulePromptOptions,
|
|
75
77
|
IFlexScopeSnapshot,
|
|
76
78
|
IFlexSession,
|
|
79
|
+
IFlexSessionGeneration,
|
|
80
|
+
IFlexSessionGenerationCohortEntry,
|
|
77
81
|
IFlexSessionReversionGroup,
|
|
78
82
|
IFlexSessionReversionInfo,
|
|
79
83
|
IFlexSessionTombstone,
|
|
@@ -173,11 +177,13 @@ import {
|
|
|
173
177
|
import { wrapToolSet } from './utils.tooloutput.js';
|
|
174
178
|
import {
|
|
175
179
|
validateCreateProjectTaskInput,
|
|
180
|
+
validateDeleteSessionGenerationCohortInput,
|
|
176
181
|
validateIdentifier,
|
|
177
182
|
validateProjectTaskId,
|
|
178
183
|
validateProjectedErrorInfo,
|
|
179
184
|
validatePromptOptions,
|
|
180
185
|
validateSlashCommandExecutionOptions,
|
|
186
|
+
validateSessionGenerationId,
|
|
181
187
|
validateUpdateProjectTaskInput,
|
|
182
188
|
validateUpdateSessionOptions,
|
|
183
189
|
validateUtf8String,
|
|
@@ -438,6 +444,16 @@ interface IFlexSubagentAcquisition {
|
|
|
438
444
|
created: boolean;
|
|
439
445
|
}
|
|
440
446
|
|
|
447
|
+
interface ISessionDeletionPlan {
|
|
448
|
+
matched: boolean;
|
|
449
|
+
deletion?: Promise<void>;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
interface IReservedSessionDeletion {
|
|
453
|
+
deletedSessions: IFlexSession[];
|
|
454
|
+
descendantRoots: string[];
|
|
455
|
+
}
|
|
456
|
+
|
|
441
457
|
type TProjectTaskToolInput =
|
|
442
458
|
| { action: 'list' }
|
|
443
459
|
| {
|
|
@@ -519,7 +535,7 @@ function requireSessionGeneration(
|
|
|
519
535
|
IFlexSession | IFlexSessionTombstone,
|
|
520
536
|
'sessionGenerationId' | 'sessionGenerationSequence'
|
|
521
537
|
>,
|
|
522
|
-
):
|
|
538
|
+
): IFlexSessionGeneration {
|
|
523
539
|
if (
|
|
524
540
|
!session.sessionGenerationId
|
|
525
541
|
|| !Number.isSafeInteger(session.sessionGenerationSequence)
|
|
@@ -533,6 +549,18 @@ function requireSessionGeneration(
|
|
|
533
549
|
};
|
|
534
550
|
}
|
|
535
551
|
|
|
552
|
+
function matchesSessionGeneration(
|
|
553
|
+
session: Pick<
|
|
554
|
+
IFlexSession | IFlexSessionTombstone,
|
|
555
|
+
'sessionGenerationId' | 'sessionGenerationSequence'
|
|
556
|
+
>,
|
|
557
|
+
expected: Readonly<IFlexSessionGeneration>,
|
|
558
|
+
): boolean {
|
|
559
|
+
const actual = requireSessionGeneration(session);
|
|
560
|
+
return actual.sessionGenerationId === expected.sessionGenerationId
|
|
561
|
+
&& actual.sessionGenerationSequence === expected.sessionGenerationSequence;
|
|
562
|
+
}
|
|
563
|
+
|
|
536
564
|
function sha256Hex(value: string): string {
|
|
537
565
|
return plugins.crypto.createHash('sha256').update(value, 'utf8').digest('hex');
|
|
538
566
|
}
|
|
@@ -726,8 +754,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
726
754
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
727
755
|
throw new FlexHarnessValidationError('createSession options must be a plain object.');
|
|
728
756
|
}
|
|
729
|
-
const unsupported = Object.keys(options).find(
|
|
757
|
+
const unsupported = Object.keys(options).find(
|
|
758
|
+
(key) => !['sessionId', 'sessionGenerationId', 'title'].includes(key),
|
|
759
|
+
);
|
|
730
760
|
if (unsupported) throw new FlexHarnessValidationError(`createSession does not support "${unsupported}".`);
|
|
761
|
+
const requestedSessionGenerationId = options.sessionGenerationId;
|
|
762
|
+
if (requestedSessionGenerationId !== undefined) {
|
|
763
|
+
validateSessionGenerationId(requestedSessionGenerationId);
|
|
764
|
+
}
|
|
731
765
|
const resolved = await this.resolveState(scopeId);
|
|
732
766
|
const sessionId = options.sessionId ?? plugins.crypto.randomUUID();
|
|
733
767
|
validateIdentifier(sessionId, 'sessionId');
|
|
@@ -774,7 +808,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
774
808
|
metadata = {
|
|
775
809
|
scopeId,
|
|
776
810
|
sessionId,
|
|
777
|
-
sessionGenerationId: createSessionGenerationId(),
|
|
811
|
+
sessionGenerationId: requestedSessionGenerationId ?? createSessionGenerationId(),
|
|
778
812
|
sessionGenerationSequence: resolved.state.revision + 1,
|
|
779
813
|
...(options.title ? { title: options.title } : {}),
|
|
780
814
|
createdAt: timestamp,
|
|
@@ -877,7 +911,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
877
911
|
completeInitialization();
|
|
878
912
|
}
|
|
879
913
|
const result = publicSnapshot(metadata);
|
|
880
|
-
this.emitEvent(scopeId,
|
|
914
|
+
this.emitEvent(scopeId, metadata, { type: 'session.created', session: result });
|
|
881
915
|
return result;
|
|
882
916
|
}
|
|
883
917
|
|
|
@@ -945,7 +979,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
945
979
|
stored.session.updatedAt = timestamp;
|
|
946
980
|
result = publicSnapshot(stored.session);
|
|
947
981
|
});
|
|
948
|
-
this.emitEvent(scopeId,
|
|
982
|
+
this.emitEvent(scopeId, result, { type: 'session.updated', session: result });
|
|
949
983
|
return result;
|
|
950
984
|
}
|
|
951
985
|
|
|
@@ -1124,17 +1158,136 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1124
1158
|
);
|
|
1125
1159
|
}
|
|
1126
1160
|
|
|
1161
|
+
public async deleteSessionGenerationCohort(
|
|
1162
|
+
scopeId: string,
|
|
1163
|
+
input: IFlexDeleteSessionGenerationCohortInput,
|
|
1164
|
+
): Promise<IFlexDeleteSessionGenerationCohortResult> {
|
|
1165
|
+
const validated = validateDeleteSessionGenerationCohortInput(input);
|
|
1166
|
+
requireTransferIdentifier(validated.root.sessionId, 'root.sessionId');
|
|
1167
|
+
for (const entry of validated.authorizedBySessionId.values()) {
|
|
1168
|
+
requireTransferIdentifier(entry.sessionId, 'authorizedCohort sessionId');
|
|
1169
|
+
}
|
|
1170
|
+
this.assertNoAmbientSlashCommandTeardown({
|
|
1171
|
+
scopeId,
|
|
1172
|
+
sessionId: validated.root.sessionId,
|
|
1173
|
+
});
|
|
1174
|
+
const { scope, state } = await this.resolveState(scopeId);
|
|
1175
|
+
this.assertNoAmbientSlashCommandTeardown({ storageKey: state.storageKey });
|
|
1176
|
+
const plan = await this.withScopeMutationOwnership(state, async (): Promise<ISessionDeletionPlan> => {
|
|
1177
|
+
const stored = state.sessions.get(validated.root.sessionId);
|
|
1178
|
+
if (stored) {
|
|
1179
|
+
if (!matchesSessionGeneration(stored.session, validated.root)) {
|
|
1180
|
+
return { matched: false };
|
|
1181
|
+
}
|
|
1182
|
+
const subtree = this.collectSessionSubtree(state, validated.root.sessionId);
|
|
1183
|
+
const descendantRoots = this.descendantTombstoneRoots(
|
|
1184
|
+
state,
|
|
1185
|
+
validated.root.sessionId,
|
|
1186
|
+
);
|
|
1187
|
+
this.assertAuthorizedSessionGenerationCohort(
|
|
1188
|
+
[
|
|
1189
|
+
...subtree.map((entry) => entry.session),
|
|
1190
|
+
...this.tombstonesForRoots(state, descendantRoots),
|
|
1191
|
+
],
|
|
1192
|
+
validated.authorizedBySessionId,
|
|
1193
|
+
);
|
|
1194
|
+
const reservation = await this.mutateScopeOwned(state, () =>
|
|
1195
|
+
this.reserveLiveSessionDeletion(
|
|
1196
|
+
state,
|
|
1197
|
+
validated.root.sessionId,
|
|
1198
|
+
stored,
|
|
1199
|
+
subtree,
|
|
1200
|
+
descendantRoots,
|
|
1201
|
+
));
|
|
1202
|
+
return {
|
|
1203
|
+
matched: true,
|
|
1204
|
+
deletion: this.startReservedSessionDeletion(
|
|
1205
|
+
state,
|
|
1206
|
+
scopeId,
|
|
1207
|
+
scope.scope,
|
|
1208
|
+
validated.root.sessionId,
|
|
1209
|
+
reservation,
|
|
1210
|
+
),
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
const tombstone = state.tombstones.get(validated.root.sessionId);
|
|
1214
|
+
if (!tombstone || !matchesSessionGeneration(tombstone, validated.root)) {
|
|
1215
|
+
return { matched: false };
|
|
1216
|
+
}
|
|
1217
|
+
const rootSessionId = tombstone.rootSessionId ?? tombstone.sessionId;
|
|
1218
|
+
const descendantRoots = this.descendantTombstoneRoots(state, rootSessionId);
|
|
1219
|
+
this.assertAuthorizedSessionGenerationCohort(
|
|
1220
|
+
this.tombstonesForRoots(state, [rootSessionId, ...descendantRoots]),
|
|
1221
|
+
validated.authorizedBySessionId,
|
|
1222
|
+
);
|
|
1223
|
+
return {
|
|
1224
|
+
matched: true,
|
|
1225
|
+
deletion: this.startSessionDeletion(
|
|
1226
|
+
state,
|
|
1227
|
+
scopeId,
|
|
1228
|
+
scope.scope,
|
|
1229
|
+
validated.root.sessionId,
|
|
1230
|
+
),
|
|
1231
|
+
};
|
|
1232
|
+
});
|
|
1233
|
+
if (!plan.matched) return publicSnapshot({ matched: false });
|
|
1234
|
+
if (!plan.deletion) throw new Error('Matched session deletion has no cleanup operation.');
|
|
1235
|
+
await plan.deletion;
|
|
1236
|
+
return publicSnapshot({ matched: true });
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1127
1239
|
public async deleteSession(scopeId: string, sessionId: string): Promise<void> {
|
|
1128
1240
|
this.assertNoAmbientSlashCommandTeardown({ scopeId, sessionId });
|
|
1129
1241
|
const { scope, state } = await this.resolveState(scopeId);
|
|
1130
1242
|
this.assertNoAmbientSlashCommandTeardown({ storageKey: state.storageKey });
|
|
1131
1243
|
validateIdentifier(sessionId, 'sessionId');
|
|
1132
1244
|
await state.scopeQueue;
|
|
1245
|
+
return this.startSessionDeletion(state, scopeId, scope.scope, sessionId);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
private startSessionDeletion(
|
|
1249
|
+
state: IStorageState,
|
|
1250
|
+
scopeId: string,
|
|
1251
|
+
scope: TScope,
|
|
1252
|
+
sessionId: string,
|
|
1253
|
+
): Promise<void> {
|
|
1133
1254
|
const deletionKey = state.tombstones.get(sessionId)?.rootSessionId ?? sessionId;
|
|
1255
|
+
return this.registerSessionDeletion(
|
|
1256
|
+
state,
|
|
1257
|
+
deletionKey,
|
|
1258
|
+
() => this.deleteSessionInternal(state, scopeId, scope, sessionId),
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
private startReservedSessionDeletion(
|
|
1263
|
+
state: IStorageState,
|
|
1264
|
+
scopeId: string,
|
|
1265
|
+
scope: TScope,
|
|
1266
|
+
sessionId: string,
|
|
1267
|
+
reservation: IReservedSessionDeletion,
|
|
1268
|
+
): Promise<void> {
|
|
1269
|
+
return this.registerSessionDeletion(
|
|
1270
|
+
state,
|
|
1271
|
+
sessionId,
|
|
1272
|
+
() => this.finishReservedSessionDeletion(
|
|
1273
|
+
state,
|
|
1274
|
+
scopeId,
|
|
1275
|
+
scope,
|
|
1276
|
+
sessionId,
|
|
1277
|
+
reservation,
|
|
1278
|
+
),
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
private registerSessionDeletion(
|
|
1283
|
+
state: IStorageState,
|
|
1284
|
+
deletionKey: string,
|
|
1285
|
+
operation: () => Promise<void>,
|
|
1286
|
+
): Promise<void> {
|
|
1134
1287
|
const existingDeletion = state.sessionDeletions.get(deletionKey);
|
|
1135
1288
|
if (existingDeletion) return existingDeletion;
|
|
1136
1289
|
let deletion!: Promise<void>;
|
|
1137
|
-
deletion =
|
|
1290
|
+
deletion = operation().finally(() => {
|
|
1138
1291
|
if (state.sessionDeletions.get(deletionKey) === deletion) {
|
|
1139
1292
|
state.sessionDeletions.delete(deletionKey);
|
|
1140
1293
|
}
|
|
@@ -1143,6 +1296,60 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1143
1296
|
return deletion;
|
|
1144
1297
|
}
|
|
1145
1298
|
|
|
1299
|
+
private assertAuthorizedSessionGenerationCohort(
|
|
1300
|
+
entries: readonly (IFlexSession | IFlexSessionTombstone)[],
|
|
1301
|
+
authorizedBySessionId: ReadonlyMap<
|
|
1302
|
+
string,
|
|
1303
|
+
Readonly<IFlexSessionGenerationCohortEntry>
|
|
1304
|
+
>,
|
|
1305
|
+
): void {
|
|
1306
|
+
for (const entry of entries) {
|
|
1307
|
+
const authorized = authorizedBySessionId.get(entry.sessionId);
|
|
1308
|
+
if (!authorized || !matchesSessionGeneration(entry, authorized)) {
|
|
1309
|
+
throw new FlexHarnessValidationError(
|
|
1310
|
+
`authorizedCohort does not authorize the exact generation of session "${entry.sessionId}".`,
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
private reserveLiveSessionDeletion(
|
|
1317
|
+
state: IStorageState,
|
|
1318
|
+
sessionId: string,
|
|
1319
|
+
stored: IStoredSessionState,
|
|
1320
|
+
subtree: readonly IStoredSessionState[],
|
|
1321
|
+
descendantRoots: readonly string[],
|
|
1322
|
+
): IReservedSessionDeletion {
|
|
1323
|
+
if (state.sessions.get(sessionId) !== stored) {
|
|
1324
|
+
throw new FlexHarnessNotFoundError('Session', sessionId);
|
|
1325
|
+
}
|
|
1326
|
+
const rootDepth = stored.session.depth ?? 0;
|
|
1327
|
+
const deletedAt = new Date().toISOString();
|
|
1328
|
+
const deletedSessions = subtree.map((entry) => cloneSerializable(entry.session));
|
|
1329
|
+
for (const entry of subtree) {
|
|
1330
|
+
const entrySessionId = entry.session.sessionId;
|
|
1331
|
+
state.sessions.delete(entrySessionId);
|
|
1332
|
+
state.retainedSessionCleanups.set(entrySessionId, {
|
|
1333
|
+
stored: entry,
|
|
1334
|
+
domainsCompleted: false,
|
|
1335
|
+
});
|
|
1336
|
+
state.tombstones.set(entrySessionId, {
|
|
1337
|
+
sessionId: entrySessionId,
|
|
1338
|
+
...requireSessionGeneration(entry.session),
|
|
1339
|
+
deletedAt,
|
|
1340
|
+
rootSessionId: sessionId,
|
|
1341
|
+
depth: (entry.session.depth ?? rootDepth) - rootDepth,
|
|
1342
|
+
...(entry.session.parentSessionId === undefined
|
|
1343
|
+
? {}
|
|
1344
|
+
: { parentSessionId: entry.session.parentSessionId }),
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
return {
|
|
1348
|
+
deletedSessions,
|
|
1349
|
+
descendantRoots: [...descendantRoots],
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1146
1353
|
private async deleteSessionInternal(
|
|
1147
1354
|
state: IStorageState,
|
|
1148
1355
|
scopeId: string,
|
|
@@ -1172,36 +1379,28 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1172
1379
|
return;
|
|
1173
1380
|
}
|
|
1174
1381
|
const stored = this.requireSession(state, sessionId);
|
|
1175
|
-
|
|
1176
|
-
let descendantRoots: string[] = [];
|
|
1177
|
-
await this.mutateScope(state, () => {
|
|
1178
|
-
if (state.sessions.get(sessionId) !== stored) {
|
|
1179
|
-
throw new FlexHarnessNotFoundError('Session', sessionId);
|
|
1180
|
-
}
|
|
1382
|
+
const reservation = await this.mutateScope(state, () => {
|
|
1181
1383
|
const subtree = this.collectSessionSubtree(state, sessionId);
|
|
1182
|
-
descendantRoots = this.descendantTombstoneRoots(state, sessionId);
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
stored: entry,
|
|
1191
|
-
domainsCompleted: false,
|
|
1192
|
-
});
|
|
1193
|
-
state.tombstones.set(entrySessionId, {
|
|
1194
|
-
sessionId: entrySessionId,
|
|
1195
|
-
...requireSessionGeneration(entry.session),
|
|
1196
|
-
deletedAt,
|
|
1197
|
-
rootSessionId: sessionId,
|
|
1198
|
-
depth: (entry.session.depth ?? rootDepth) - rootDepth,
|
|
1199
|
-
...(entry.session.parentSessionId === undefined
|
|
1200
|
-
? {}
|
|
1201
|
-
: { parentSessionId: entry.session.parentSessionId }),
|
|
1202
|
-
});
|
|
1203
|
-
}
|
|
1384
|
+
const descendantRoots = this.descendantTombstoneRoots(state, sessionId);
|
|
1385
|
+
return this.reserveLiveSessionDeletion(
|
|
1386
|
+
state,
|
|
1387
|
+
sessionId,
|
|
1388
|
+
stored,
|
|
1389
|
+
subtree,
|
|
1390
|
+
descendantRoots,
|
|
1391
|
+
);
|
|
1204
1392
|
});
|
|
1393
|
+
await this.finishReservedSessionDeletion(state, scopeId, scope, sessionId, reservation);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
private async finishReservedSessionDeletion(
|
|
1397
|
+
state: IStorageState,
|
|
1398
|
+
scopeId: string,
|
|
1399
|
+
scope: TScope,
|
|
1400
|
+
sessionId: string,
|
|
1401
|
+
reservation: IReservedSessionDeletion,
|
|
1402
|
+
): Promise<void> {
|
|
1403
|
+
const { deletedSessions, descendantRoots } = reservation;
|
|
1205
1404
|
const reason = this.trustInternalError(new FlexHarnessAbortError('The session subtree was deleted.'));
|
|
1206
1405
|
const slashCommandErrors: unknown[] = [];
|
|
1207
1406
|
for (const deleted of deletedSessions) {
|
|
@@ -1250,7 +1449,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1250
1449
|
throw this.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-delete');
|
|
1251
1450
|
}
|
|
1252
1451
|
for (const deleted of childFirstDeletedSessions) {
|
|
1253
|
-
this.emitEvent(scopeId, deleted
|
|
1452
|
+
this.emitEvent(scopeId, deleted, {
|
|
1254
1453
|
type: 'session.deleted',
|
|
1255
1454
|
session: publicSnapshot(deleted),
|
|
1256
1455
|
});
|
|
@@ -1849,6 +2048,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1849
2048
|
scope,
|
|
1850
2049
|
storageKey: state.storageKey,
|
|
1851
2050
|
sessionId: stored.session.sessionId,
|
|
2051
|
+
...requireSessionGeneration(stored.session),
|
|
1852
2052
|
rawArguments,
|
|
1853
2053
|
arguments: arguments_,
|
|
1854
2054
|
signal,
|
|
@@ -2414,7 +2614,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2414
2614
|
.find((segment) => segment?.status === 'completed');
|
|
2415
2615
|
if (!target) throw new FlexHarnessValidationError('Pending reversion has no target candidate.');
|
|
2416
2616
|
await this.resumePendingApply(state, stored, scopeId, scope, operationSignal);
|
|
2417
|
-
this.emitEvent(scopeId,
|
|
2617
|
+
this.emitEvent(scopeId, stored.session, {
|
|
2418
2618
|
type: 'session.history.changed',
|
|
2419
2619
|
direction,
|
|
2420
2620
|
runId: target.runId,
|
|
@@ -2465,7 +2665,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2465
2665
|
scope,
|
|
2466
2666
|
operationSignal,
|
|
2467
2667
|
);
|
|
2468
|
-
this.emitEvent(scopeId,
|
|
2668
|
+
this.emitEvent(scopeId, stored.session, {
|
|
2469
2669
|
type: 'session.history.changed',
|
|
2470
2670
|
direction,
|
|
2471
2671
|
runId: unit.target.runId,
|
|
@@ -2512,6 +2712,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2512
2712
|
): Promise<void> {
|
|
2513
2713
|
const pending = stored.pendingReversion;
|
|
2514
2714
|
if (pending?.kind !== 'apply') return;
|
|
2715
|
+
const sessionGeneration = requireSessionGeneration(stored.session);
|
|
2515
2716
|
const segmentMap = new Map(stored.reversionSegments.map((segment) => [segment.runId, segment]));
|
|
2516
2717
|
if (
|
|
2517
2718
|
pending.segmentRunIds.some((runId) =>
|
|
@@ -2552,6 +2753,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2552
2753
|
scope,
|
|
2553
2754
|
storageKey: state.storageKey,
|
|
2554
2755
|
sessionId: stored.session.sessionId,
|
|
2756
|
+
...sessionGeneration,
|
|
2555
2757
|
runId,
|
|
2556
2758
|
captureId,
|
|
2557
2759
|
reference: cloneSerializable(reference),
|
|
@@ -2685,6 +2887,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2685
2887
|
scopeId,
|
|
2686
2888
|
scope,
|
|
2687
2889
|
stored.session.sessionId,
|
|
2890
|
+
requireSessionGeneration(stored.session),
|
|
2688
2891
|
pending.runId,
|
|
2689
2892
|
pending.captureId,
|
|
2690
2893
|
pending.state === 'prepared' ? 'finalizing' : pending.state,
|
|
@@ -2760,7 +2963,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2760
2963
|
throw this.projectOperationError(error, 'persistence', scopeId, sessionId, 'archive-horizon');
|
|
2761
2964
|
}
|
|
2762
2965
|
if (branchCommitted) {
|
|
2763
|
-
this.emitEvent(scopeId,
|
|
2966
|
+
this.emitEvent(scopeId, stored.session, {
|
|
2764
2967
|
type: 'session.history.changed',
|
|
2765
2968
|
direction: 'branch',
|
|
2766
2969
|
});
|
|
@@ -3226,14 +3429,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3226
3429
|
}
|
|
3227
3430
|
|
|
3228
3431
|
const runType = run.scheduleKey ? 'run.scheduled' : 'run.started';
|
|
3229
|
-
this.emitEvent(run.scopeId, run.
|
|
3432
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
3230
3433
|
type: runType,
|
|
3231
3434
|
runId: run.runId,
|
|
3232
3435
|
messageId: run.userMessageId,
|
|
3233
3436
|
session: publicSnapshot(run.stored.session),
|
|
3234
3437
|
});
|
|
3235
3438
|
for (const message of [run.reservedUserMessage!, run.reservedAssistantMessage!]) {
|
|
3236
|
-
this.emitEvent(run.scopeId, run.
|
|
3439
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
3237
3440
|
type: 'message.created',
|
|
3238
3441
|
runId: run.runId,
|
|
3239
3442
|
messageId: message.messageId,
|
|
@@ -3302,7 +3505,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3302
3505
|
type: 'prompt.queued' | 'prompt.started' | 'prompt.running' | 'prompt.finished',
|
|
3303
3506
|
entry: IFlexPromptQueueEntry = this.projectPromptQueueEntry(queued),
|
|
3304
3507
|
): void {
|
|
3305
|
-
this.emitEvent(queued.scopeId, queued.
|
|
3508
|
+
this.emitEvent(queued.scopeId, queued.stored.session, {
|
|
3306
3509
|
type,
|
|
3307
3510
|
queueId: queued.queueId,
|
|
3308
3511
|
...(entry.runId ? { runId: entry.runId } : {}),
|
|
@@ -3986,6 +4189,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3986
4189
|
scopeId: run.scopeId,
|
|
3987
4190
|
scope: run.scope as TScope,
|
|
3988
4191
|
sessionId: run.sessionId,
|
|
4192
|
+
...requireSessionGeneration(run.stored.session),
|
|
3989
4193
|
runId: run.runId,
|
|
3990
4194
|
...(options.modelHint ? { modelHint: options.modelHint } : {}),
|
|
3991
4195
|
...resolverRelationship,
|
|
@@ -3999,6 +4203,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3999
4203
|
scopeId: run.scopeId,
|
|
4000
4204
|
scope: run.scope as TScope,
|
|
4001
4205
|
sessionId: run.sessionId,
|
|
4206
|
+
...requireSessionGeneration(run.stored.session),
|
|
4002
4207
|
runId: run.runId,
|
|
4003
4208
|
...resolverRelationship,
|
|
4004
4209
|
signal,
|
|
@@ -4184,7 +4389,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4184
4389
|
run.stored.session.updatedAt = new Date().toISOString();
|
|
4185
4390
|
session = publicSnapshot(run.stored.session);
|
|
4186
4391
|
});
|
|
4187
|
-
this.emitEvent(run.scopeId,
|
|
4392
|
+
this.emitEvent(run.scopeId, session, {
|
|
4188
4393
|
type: 'session.updated',
|
|
4189
4394
|
session,
|
|
4190
4395
|
});
|
|
@@ -4716,7 +4921,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4716
4921
|
throw aborted;
|
|
4717
4922
|
}
|
|
4718
4923
|
state.sessions.set(sessionId, loaded);
|
|
4719
|
-
this.emitEvent(run.scopeId,
|
|
4924
|
+
this.emitEvent(run.scopeId, metadata, {
|
|
4720
4925
|
type: 'session.created',
|
|
4721
4926
|
session: publicSnapshot(metadata),
|
|
4722
4927
|
});
|
|
@@ -4967,6 +5172,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4967
5172
|
scope: run.scope as TScope,
|
|
4968
5173
|
storageKey: run.state.storageKey,
|
|
4969
5174
|
sessionId: run.sessionId,
|
|
5175
|
+
...requireSessionGeneration(run.stored.session),
|
|
4970
5176
|
runId: run.runId,
|
|
4971
5177
|
captureId,
|
|
4972
5178
|
signal,
|
|
@@ -5001,6 +5207,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5001
5207
|
scope: TScope,
|
|
5002
5208
|
storageKey: string,
|
|
5003
5209
|
sessionId: string,
|
|
5210
|
+
sessionGeneration: Readonly<IFlexSessionGeneration>,
|
|
5004
5211
|
runId: string,
|
|
5005
5212
|
captureId: string,
|
|
5006
5213
|
): Promise<TNormalizedCaptureInspection> {
|
|
@@ -5011,6 +5218,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5011
5218
|
scope,
|
|
5012
5219
|
storageKey,
|
|
5013
5220
|
sessionId,
|
|
5221
|
+
...sessionGeneration,
|
|
5014
5222
|
runId,
|
|
5015
5223
|
captureId,
|
|
5016
5224
|
signal,
|
|
@@ -5044,6 +5252,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5044
5252
|
scope: TScope,
|
|
5045
5253
|
storageKey: string,
|
|
5046
5254
|
sessionId: string,
|
|
5255
|
+
sessionGeneration: Readonly<IFlexSessionGeneration>,
|
|
5047
5256
|
runId: string,
|
|
5048
5257
|
captureId: string,
|
|
5049
5258
|
): Promise<TFlexTurnReversionFinalizedOutcome> {
|
|
@@ -5053,6 +5262,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5053
5262
|
scope,
|
|
5054
5263
|
storageKey,
|
|
5055
5264
|
sessionId,
|
|
5265
|
+
...sessionGeneration,
|
|
5056
5266
|
runId,
|
|
5057
5267
|
captureId,
|
|
5058
5268
|
signal,
|
|
@@ -5072,6 +5282,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5072
5282
|
scopeId: string,
|
|
5073
5283
|
scope: TScope,
|
|
5074
5284
|
sessionId: string,
|
|
5285
|
+
sessionGeneration: Readonly<IFlexSessionGeneration>,
|
|
5075
5286
|
runId: string,
|
|
5076
5287
|
captureId: string,
|
|
5077
5288
|
durableState: 'preparing' | 'prepared' | 'finalizing',
|
|
@@ -5083,6 +5294,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5083
5294
|
scope,
|
|
5084
5295
|
state.storageKey,
|
|
5085
5296
|
sessionId,
|
|
5297
|
+
sessionGeneration,
|
|
5086
5298
|
runId,
|
|
5087
5299
|
captureId,
|
|
5088
5300
|
);
|
|
@@ -5107,6 +5319,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5107
5319
|
scope,
|
|
5108
5320
|
state.storageKey,
|
|
5109
5321
|
sessionId,
|
|
5322
|
+
sessionGeneration,
|
|
5110
5323
|
runId,
|
|
5111
5324
|
captureId,
|
|
5112
5325
|
);
|
|
@@ -5116,6 +5329,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5116
5329
|
scope,
|
|
5117
5330
|
state.storageKey,
|
|
5118
5331
|
sessionId,
|
|
5332
|
+
sessionGeneration,
|
|
5119
5333
|
runId,
|
|
5120
5334
|
captureId,
|
|
5121
5335
|
);
|
|
@@ -5268,6 +5482,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5268
5482
|
run.scope as TScope,
|
|
5269
5483
|
run.state.storageKey,
|
|
5270
5484
|
run.sessionId,
|
|
5485
|
+
requireSessionGeneration(run.stored.session),
|
|
5271
5486
|
run.runId,
|
|
5272
5487
|
captureId,
|
|
5273
5488
|
);
|
|
@@ -5349,6 +5564,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5349
5564
|
run.scopeId,
|
|
5350
5565
|
run.scope as TScope,
|
|
5351
5566
|
run.sessionId,
|
|
5567
|
+
requireSessionGeneration(run.stored.session),
|
|
5352
5568
|
run.runId,
|
|
5353
5569
|
captureId,
|
|
5354
5570
|
pending.state === 'prepared' ? 'finalizing' : pending.state,
|
|
@@ -5514,7 +5730,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5514
5730
|
return;
|
|
5515
5731
|
}
|
|
5516
5732
|
await this.mutateProjection(state, stored, () => this.commitRevertedBranchState(stored), true);
|
|
5517
|
-
this.emitEvent(scopeId, stored.session
|
|
5733
|
+
this.emitEvent(scopeId, stored.session, {
|
|
5518
5734
|
type: 'session.history.changed',
|
|
5519
5735
|
direction: 'branch',
|
|
5520
5736
|
});
|
|
@@ -5598,6 +5814,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5598
5814
|
}
|
|
5599
5815
|
return;
|
|
5600
5816
|
}
|
|
5817
|
+
const sessionGeneration = requireSessionGeneration(stored.session);
|
|
5601
5818
|
while (stored.pendingReversionReleases.length > 0) {
|
|
5602
5819
|
const release = stored.pendingReversionReleases[0];
|
|
5603
5820
|
if (this.reversionProviderProtocolVersion() !== release.protocolVersion) {
|
|
@@ -5612,6 +5829,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5612
5829
|
scope,
|
|
5613
5830
|
storageKey: state.storageKey,
|
|
5614
5831
|
sessionId: stored.session.sessionId,
|
|
5832
|
+
...sessionGeneration,
|
|
5615
5833
|
runId: release.runId,
|
|
5616
5834
|
captureId: release.captureId,
|
|
5617
5835
|
reference: cloneSerializable(release.reference),
|
|
@@ -5649,11 +5867,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5649
5867
|
private async releaseDeletedSessionReversions(
|
|
5650
5868
|
state: IStorageState,
|
|
5651
5869
|
storageKey: string,
|
|
5652
|
-
|
|
5870
|
+
tombstone: IFlexSessionTombstone,
|
|
5653
5871
|
scopeId: string,
|
|
5654
5872
|
scope: TScope,
|
|
5655
5873
|
stored?: IStoredSessionState,
|
|
5656
5874
|
): Promise<void> {
|
|
5875
|
+
const sessionId = tombstone.sessionId;
|
|
5876
|
+
const sessionGeneration = requireSessionGeneration(stored?.session ?? tombstone);
|
|
5657
5877
|
if (stored) await stored.projectionQueue;
|
|
5658
5878
|
let durableProjection = await this.stores.projections.load(storageKey, sessionId);
|
|
5659
5879
|
if (!durableProjection) return;
|
|
@@ -5712,6 +5932,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5712
5932
|
scope,
|
|
5713
5933
|
storageKey,
|
|
5714
5934
|
sessionId,
|
|
5935
|
+
...sessionGeneration,
|
|
5715
5936
|
runId,
|
|
5716
5937
|
captureId,
|
|
5717
5938
|
reference: cloneSerializable(reference),
|
|
@@ -5825,6 +6046,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5825
6046
|
scope,
|
|
5826
6047
|
storageKey,
|
|
5827
6048
|
sessionId,
|
|
6049
|
+
sessionGeneration,
|
|
5828
6050
|
capture.runId,
|
|
5829
6051
|
capture.captureId,
|
|
5830
6052
|
);
|
|
@@ -5854,6 +6076,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5854
6076
|
scope,
|
|
5855
6077
|
storageKey,
|
|
5856
6078
|
sessionId,
|
|
6079
|
+
sessionGeneration,
|
|
5857
6080
|
capture.runId,
|
|
5858
6081
|
capture.captureId,
|
|
5859
6082
|
);
|
|
@@ -5863,6 +6086,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5863
6086
|
scope,
|
|
5864
6087
|
storageKey,
|
|
5865
6088
|
sessionId,
|
|
6089
|
+
sessionGeneration,
|
|
5866
6090
|
capture.runId,
|
|
5867
6091
|
capture.captureId,
|
|
5868
6092
|
);
|
|
@@ -5928,6 +6152,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5928
6152
|
scope,
|
|
5929
6153
|
storageKey,
|
|
5930
6154
|
sessionId,
|
|
6155
|
+
...sessionGeneration,
|
|
5931
6156
|
runId: release.runId,
|
|
5932
6157
|
captureId: release.captureId,
|
|
5933
6158
|
reference: cloneSerializable(release.reference),
|
|
@@ -6031,7 +6256,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6031
6256
|
}
|
|
6032
6257
|
}
|
|
6033
6258
|
for (const message of [terminal.userMessage, terminal.assistantMessage]) {
|
|
6034
|
-
this.emitEvent(run.scopeId, run.
|
|
6259
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
6035
6260
|
type: 'message.updated',
|
|
6036
6261
|
runId: run.runId,
|
|
6037
6262
|
messageId: message.messageId,
|
|
@@ -6049,7 +6274,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6049
6274
|
const status: IFlexRunFinishedEvent['status'] = error
|
|
6050
6275
|
? cancelled ? 'cancelled' : 'failed'
|
|
6051
6276
|
: 'completed';
|
|
6052
|
-
this.emitEvent(run.scopeId, run.
|
|
6277
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
6053
6278
|
type: 'run.finished',
|
|
6054
6279
|
runId: run.runId,
|
|
6055
6280
|
messageId: run.assistantMessageId,
|
|
@@ -6307,6 +6532,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6307
6532
|
permissionId: plugins.crypto.randomUUID(),
|
|
6308
6533
|
scopeId: run.scopeId,
|
|
6309
6534
|
sessionId: run.sessionId,
|
|
6535
|
+
...requireSessionGeneration(run.stored.session),
|
|
6310
6536
|
runId: run.runId,
|
|
6311
6537
|
kind: input.kind,
|
|
6312
6538
|
description: truncateUtf8(input.description, maxTransferMetadataBytes),
|
|
@@ -6355,7 +6581,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6355
6581
|
return promise;
|
|
6356
6582
|
}
|
|
6357
6583
|
if (!pending.settled) {
|
|
6358
|
-
this.emitEvent(run.scopeId, run.
|
|
6584
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
6359
6585
|
type: 'permission.requested',
|
|
6360
6586
|
runId: run.runId,
|
|
6361
6587
|
request: publicSnapshot(request),
|
|
@@ -6456,7 +6682,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6456
6682
|
state.pendingPermissions.delete(pending.request.permissionId);
|
|
6457
6683
|
pending.run.pendingPermissionIds.delete(pending.request.permissionId);
|
|
6458
6684
|
pending.run.controller.signal.removeEventListener('abort', pending.abortListener);
|
|
6459
|
-
this.emitEvent(pending.request.scopeId, pending.
|
|
6685
|
+
this.emitEvent(pending.request.scopeId, pending.run.stored.session, {
|
|
6460
6686
|
type: 'permission.resolved',
|
|
6461
6687
|
runId: pending.request.runId,
|
|
6462
6688
|
request: publicSnapshot(pending.request),
|
|
@@ -6707,6 +6933,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6707
6933
|
scopeId: context.scopeId,
|
|
6708
6934
|
scope: context.scope,
|
|
6709
6935
|
sessionId: context.sessionId,
|
|
6936
|
+
...requireSessionGeneration(run.stored.session),
|
|
6710
6937
|
runId: context.runId,
|
|
6711
6938
|
...(context.parentSessionId === undefined ? {} : { parentSessionId: context.parentSessionId }),
|
|
6712
6939
|
...(context.agent === undefined ? {} : { agent: context.agent }),
|
|
@@ -7116,7 +7343,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7116
7343
|
await this.releaseDeletedSessionReversions(
|
|
7117
7344
|
state,
|
|
7118
7345
|
storageKey,
|
|
7119
|
-
tombstone
|
|
7346
|
+
tombstone,
|
|
7120
7347
|
scopeId,
|
|
7121
7348
|
scope,
|
|
7122
7349
|
);
|
|
@@ -7690,93 +7917,112 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7690
7917
|
};
|
|
7691
7918
|
}
|
|
7692
7919
|
|
|
7920
|
+
private withScopeMutationOwnership<TValue>(
|
|
7921
|
+
state: IStorageState,
|
|
7922
|
+
operation: () => TValue | Promise<TValue>,
|
|
7923
|
+
): Promise<TValue> {
|
|
7924
|
+
const owned = state.scopeQueue.then(async (): Promise<TValue> => {
|
|
7925
|
+
this.assertStateAcceptingWork(state);
|
|
7926
|
+
return operation();
|
|
7927
|
+
});
|
|
7928
|
+
state.scopeQueue = owned.then(() => undefined, () => undefined);
|
|
7929
|
+
return owned;
|
|
7930
|
+
}
|
|
7931
|
+
|
|
7693
7932
|
private mutateScope<TValue>(
|
|
7694
7933
|
state: IStorageState,
|
|
7695
7934
|
mutation: () => TValue,
|
|
7696
7935
|
allowDuringDrain = false,
|
|
7697
7936
|
): Promise<TValue> {
|
|
7698
|
-
const operation = state.scopeQueue.then(
|
|
7937
|
+
const operation = state.scopeQueue.then(() => {
|
|
7699
7938
|
if (!allowDuringDrain) this.assertStateAcceptingWork(state);
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7939
|
+
return this.mutateScopeOwned(state, mutation);
|
|
7940
|
+
});
|
|
7941
|
+
state.scopeQueue = operation.then(() => undefined, () => undefined);
|
|
7942
|
+
return operation;
|
|
7943
|
+
}
|
|
7944
|
+
|
|
7945
|
+
private async mutateScopeOwned<TValue>(
|
|
7946
|
+
state: IStorageState,
|
|
7947
|
+
mutation: () => TValue,
|
|
7948
|
+
): Promise<TValue> {
|
|
7949
|
+
const beforeRevision = state.revision;
|
|
7950
|
+
const beforeSessions = new Map([...state.sessions].map(([id, stored]) => [id, {
|
|
7951
|
+
stored,
|
|
7952
|
+
metadata: cloneSerializable(stored.session),
|
|
7953
|
+
}]));
|
|
7954
|
+
const beforeTombstones = new Map([...state.tombstones].map(([id, value]) => [id, cloneSerializable(value)]));
|
|
7955
|
+
const beforeRetainedCleanups = new Map(state.retainedSessionCleanups);
|
|
7956
|
+
const beforeSnapshot: IFlexScopeSnapshot = {
|
|
7957
|
+
schemaVersion: 1,
|
|
7958
|
+
revision: beforeRevision,
|
|
7959
|
+
sessions: [...beforeSessions.values()]
|
|
7960
|
+
.map((entry) => cloneSerializable(entry.metadata))
|
|
7961
|
+
.sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
|
|
7962
|
+
tombstones: [...beforeTombstones.values()]
|
|
7963
|
+
.map((value) => cloneSerializable(value))
|
|
7964
|
+
.sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
|
|
7965
|
+
};
|
|
7966
|
+
const restore = () => {
|
|
7967
|
+
state.revision = beforeRevision;
|
|
7968
|
+
state.sessions.clear();
|
|
7969
|
+
for (const [id, entry] of beforeSessions) {
|
|
7970
|
+
entry.stored.session = entry.metadata;
|
|
7971
|
+
state.sessions.set(id, entry.stored);
|
|
7972
|
+
}
|
|
7973
|
+
state.tombstones.clear();
|
|
7974
|
+
for (const [id, value] of beforeTombstones) state.tombstones.set(id, value);
|
|
7975
|
+
state.retainedSessionCleanups.clear();
|
|
7976
|
+
for (const [id, retained] of beforeRetainedCleanups) {
|
|
7977
|
+
state.retainedSessionCleanups.set(id, retained);
|
|
7978
|
+
}
|
|
7979
|
+
};
|
|
7980
|
+
let result: TValue;
|
|
7981
|
+
try {
|
|
7982
|
+
result = mutation();
|
|
7983
|
+
} catch (error) {
|
|
7984
|
+
restore();
|
|
7985
|
+
throw error;
|
|
7986
|
+
}
|
|
7987
|
+
const snapshot = this.createScopeSnapshot(state, beforeRevision + 1);
|
|
7988
|
+
try {
|
|
7989
|
+
await this.stores.scopes.save(state.storageKey, snapshot, beforeRevision);
|
|
7990
|
+
state.revision = snapshot.revision;
|
|
7991
|
+
return result;
|
|
7992
|
+
} catch (error) {
|
|
7993
|
+
if (error instanceof FlexHarnessStoreConflictError) {
|
|
7735
7994
|
restore();
|
|
7736
7995
|
throw error;
|
|
7737
7996
|
}
|
|
7738
|
-
|
|
7739
|
-
try {
|
|
7740
|
-
await this.stores.scopes.save(state.storageKey, snapshot, beforeRevision);
|
|
7741
|
-
state.revision = snapshot.revision;
|
|
7742
|
-
return result;
|
|
7743
|
-
} catch (error) {
|
|
7744
|
-
if (error instanceof FlexHarnessStoreConflictError) {
|
|
7745
|
-
restore();
|
|
7746
|
-
throw error;
|
|
7747
|
-
}
|
|
7748
|
-
if (error instanceof FlexHarnessStoreCommitUncertainError) {
|
|
7749
|
-
state.revision = snapshot.revision;
|
|
7750
|
-
state.lifecycle = 'fenced';
|
|
7751
|
-
throw error;
|
|
7752
|
-
}
|
|
7753
|
-
let current: IFlexScopeSnapshot | undefined;
|
|
7754
|
-
let reconciliationError: unknown;
|
|
7755
|
-
try {
|
|
7756
|
-
current = await this.stores.scopes.load(state.storageKey);
|
|
7757
|
-
} catch (loadError) {
|
|
7758
|
-
reconciliationError = loadError;
|
|
7759
|
-
}
|
|
7760
|
-
if (current && JSON.stringify(current) === JSON.stringify(snapshot)) {
|
|
7761
|
-
state.revision = snapshot.revision;
|
|
7762
|
-
throw error;
|
|
7763
|
-
}
|
|
7764
|
-
if (
|
|
7765
|
-
(current === undefined && beforeRevision === 0)
|
|
7766
|
-
|| (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot))
|
|
7767
|
-
) {
|
|
7768
|
-
restore();
|
|
7769
|
-
throw error;
|
|
7770
|
-
}
|
|
7997
|
+
if (error instanceof FlexHarnessStoreCommitUncertainError) {
|
|
7771
7998
|
state.revision = snapshot.revision;
|
|
7772
7999
|
state.lifecycle = 'fenced';
|
|
7773
|
-
throw
|
|
7774
|
-
? error
|
|
7775
|
-
: combineErrors([error, reconciliationError]);
|
|
8000
|
+
throw error;
|
|
7776
8001
|
}
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
8002
|
+
let current: IFlexScopeSnapshot | undefined;
|
|
8003
|
+
let reconciliationError: unknown;
|
|
8004
|
+
try {
|
|
8005
|
+
current = await this.stores.scopes.load(state.storageKey);
|
|
8006
|
+
} catch (loadError) {
|
|
8007
|
+
reconciliationError = loadError;
|
|
8008
|
+
}
|
|
8009
|
+
if (current && JSON.stringify(current) === JSON.stringify(snapshot)) {
|
|
8010
|
+
state.revision = snapshot.revision;
|
|
8011
|
+
throw error;
|
|
8012
|
+
}
|
|
8013
|
+
if (
|
|
8014
|
+
(current === undefined && beforeRevision === 0)
|
|
8015
|
+
|| (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot))
|
|
8016
|
+
) {
|
|
8017
|
+
restore();
|
|
8018
|
+
throw error;
|
|
8019
|
+
}
|
|
8020
|
+
state.revision = snapshot.revision;
|
|
8021
|
+
state.lifecycle = 'fenced';
|
|
8022
|
+
throw reconciliationError === undefined
|
|
8023
|
+
? error
|
|
8024
|
+
: combineErrors([error, reconciliationError]);
|
|
8025
|
+
}
|
|
7780
8026
|
}
|
|
7781
8027
|
|
|
7782
8028
|
private mutateProjection<TValue>(
|
|
@@ -8210,6 +8456,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8210
8456
|
|| left.sessionId.localeCompare(right.sessionId));
|
|
8211
8457
|
}
|
|
8212
8458
|
|
|
8459
|
+
private tombstonesForRoots(
|
|
8460
|
+
state: IStorageState,
|
|
8461
|
+
rootSessionIds: readonly string[],
|
|
8462
|
+
): IFlexSessionTombstone[] {
|
|
8463
|
+
return [...new Set(rootSessionIds)]
|
|
8464
|
+
.flatMap((rootSessionId) => this.tombstoneGroup(state, rootSessionId));
|
|
8465
|
+
}
|
|
8466
|
+
|
|
8213
8467
|
private descendantTombstoneRoots(
|
|
8214
8468
|
state: IStorageState,
|
|
8215
8469
|
ancestorSessionId: string,
|
|
@@ -8362,7 +8616,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8362
8616
|
await this.releaseDeletedSessionReversions(
|
|
8363
8617
|
state,
|
|
8364
8618
|
state.storageKey,
|
|
8365
|
-
|
|
8619
|
+
tombstone,
|
|
8366
8620
|
releaseContext.scopeId,
|
|
8367
8621
|
releaseContext.scope,
|
|
8368
8622
|
retained?.stored,
|
|
@@ -9390,7 +9644,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9390
9644
|
part: TFlexMessagePart,
|
|
9391
9645
|
): void {
|
|
9392
9646
|
const { messageIndex, partIndex } = this.partEventCoordinates(run, part.partId);
|
|
9393
|
-
this.emitEvent(run.scopeId, run.
|
|
9647
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
9394
9648
|
type,
|
|
9395
9649
|
runId: run.runId,
|
|
9396
9650
|
messageId: run.assistantMessageId,
|
|
@@ -9408,7 +9662,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9408
9662
|
accounting: { baseTextUtf8Bytes: number; textUtf8Bytes: number },
|
|
9409
9663
|
): void {
|
|
9410
9664
|
const { messageIndex, partIndex } = this.partEventCoordinates(run, part.partId);
|
|
9411
|
-
this.emitEvent(run.scopeId, run.
|
|
9665
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
9412
9666
|
type: 'part.delta',
|
|
9413
9667
|
runId: run.runId,
|
|
9414
9668
|
messageId: run.assistantMessageId,
|
|
@@ -9438,14 +9692,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9438
9692
|
return { messageIndex, partIndex };
|
|
9439
9693
|
}
|
|
9440
9694
|
|
|
9441
|
-
private emitEvent(scopeId: string,
|
|
9695
|
+
private emitEvent(scopeId: string, sourceSession: IFlexSession, details: TEventDetails): void {
|
|
9696
|
+
const generation = requireSessionGeneration(sourceSession);
|
|
9442
9697
|
const event = deepFreeze(cloneSerializable({
|
|
9443
9698
|
eventId: plugins.crypto.randomUUID(),
|
|
9444
9699
|
sequence: ++this.sequence,
|
|
9445
9700
|
timestamp: new Date().toISOString(),
|
|
9446
9701
|
scopeId,
|
|
9447
|
-
sessionId,
|
|
9448
9702
|
...details,
|
|
9703
|
+
sessionId: sourceSession.sessionId,
|
|
9704
|
+
...generation,
|
|
9449
9705
|
})) as TFlexHarnessEvent;
|
|
9450
9706
|
for (const listener of this.listeners) {
|
|
9451
9707
|
try {
|