@modelprofile.com/flexharness 5.1.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 +9 -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 +231 -128
- package/dist_ts/interfaces.d.ts +16 -3
- 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 +14 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +355 -126
- package/ts/interfaces.ts +20 -3
- 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,
|
|
@@ -75,6 +77,7 @@ import type {
|
|
|
75
77
|
IFlexScopeSnapshot,
|
|
76
78
|
IFlexSession,
|
|
77
79
|
IFlexSessionGeneration,
|
|
80
|
+
IFlexSessionGenerationCohortEntry,
|
|
78
81
|
IFlexSessionReversionGroup,
|
|
79
82
|
IFlexSessionReversionInfo,
|
|
80
83
|
IFlexSessionTombstone,
|
|
@@ -174,11 +177,13 @@ import {
|
|
|
174
177
|
import { wrapToolSet } from './utils.tooloutput.js';
|
|
175
178
|
import {
|
|
176
179
|
validateCreateProjectTaskInput,
|
|
180
|
+
validateDeleteSessionGenerationCohortInput,
|
|
177
181
|
validateIdentifier,
|
|
178
182
|
validateProjectTaskId,
|
|
179
183
|
validateProjectedErrorInfo,
|
|
180
184
|
validatePromptOptions,
|
|
181
185
|
validateSlashCommandExecutionOptions,
|
|
186
|
+
validateSessionGenerationId,
|
|
182
187
|
validateUpdateProjectTaskInput,
|
|
183
188
|
validateUpdateSessionOptions,
|
|
184
189
|
validateUtf8String,
|
|
@@ -439,6 +444,16 @@ interface IFlexSubagentAcquisition {
|
|
|
439
444
|
created: boolean;
|
|
440
445
|
}
|
|
441
446
|
|
|
447
|
+
interface ISessionDeletionPlan {
|
|
448
|
+
matched: boolean;
|
|
449
|
+
deletion?: Promise<void>;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
interface IReservedSessionDeletion {
|
|
453
|
+
deletedSessions: IFlexSession[];
|
|
454
|
+
descendantRoots: string[];
|
|
455
|
+
}
|
|
456
|
+
|
|
442
457
|
type TProjectTaskToolInput =
|
|
443
458
|
| { action: 'list' }
|
|
444
459
|
| {
|
|
@@ -534,6 +549,18 @@ function requireSessionGeneration(
|
|
|
534
549
|
};
|
|
535
550
|
}
|
|
536
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
|
+
|
|
537
564
|
function sha256Hex(value: string): string {
|
|
538
565
|
return plugins.crypto.createHash('sha256').update(value, 'utf8').digest('hex');
|
|
539
566
|
}
|
|
@@ -727,8 +754,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
727
754
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
728
755
|
throw new FlexHarnessValidationError('createSession options must be a plain object.');
|
|
729
756
|
}
|
|
730
|
-
const unsupported = Object.keys(options).find(
|
|
757
|
+
const unsupported = Object.keys(options).find(
|
|
758
|
+
(key) => !['sessionId', 'sessionGenerationId', 'title'].includes(key),
|
|
759
|
+
);
|
|
731
760
|
if (unsupported) throw new FlexHarnessValidationError(`createSession does not support "${unsupported}".`);
|
|
761
|
+
const requestedSessionGenerationId = options.sessionGenerationId;
|
|
762
|
+
if (requestedSessionGenerationId !== undefined) {
|
|
763
|
+
validateSessionGenerationId(requestedSessionGenerationId);
|
|
764
|
+
}
|
|
732
765
|
const resolved = await this.resolveState(scopeId);
|
|
733
766
|
const sessionId = options.sessionId ?? plugins.crypto.randomUUID();
|
|
734
767
|
validateIdentifier(sessionId, 'sessionId');
|
|
@@ -775,7 +808,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
775
808
|
metadata = {
|
|
776
809
|
scopeId,
|
|
777
810
|
sessionId,
|
|
778
|
-
sessionGenerationId: createSessionGenerationId(),
|
|
811
|
+
sessionGenerationId: requestedSessionGenerationId ?? createSessionGenerationId(),
|
|
779
812
|
sessionGenerationSequence: resolved.state.revision + 1,
|
|
780
813
|
...(options.title ? { title: options.title } : {}),
|
|
781
814
|
createdAt: timestamp,
|
|
@@ -878,7 +911,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
878
911
|
completeInitialization();
|
|
879
912
|
}
|
|
880
913
|
const result = publicSnapshot(metadata);
|
|
881
|
-
this.emitEvent(scopeId,
|
|
914
|
+
this.emitEvent(scopeId, metadata, { type: 'session.created', session: result });
|
|
882
915
|
return result;
|
|
883
916
|
}
|
|
884
917
|
|
|
@@ -946,7 +979,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
946
979
|
stored.session.updatedAt = timestamp;
|
|
947
980
|
result = publicSnapshot(stored.session);
|
|
948
981
|
});
|
|
949
|
-
this.emitEvent(scopeId,
|
|
982
|
+
this.emitEvent(scopeId, result, { type: 'session.updated', session: result });
|
|
950
983
|
return result;
|
|
951
984
|
}
|
|
952
985
|
|
|
@@ -1125,17 +1158,136 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1125
1158
|
);
|
|
1126
1159
|
}
|
|
1127
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
|
+
|
|
1128
1239
|
public async deleteSession(scopeId: string, sessionId: string): Promise<void> {
|
|
1129
1240
|
this.assertNoAmbientSlashCommandTeardown({ scopeId, sessionId });
|
|
1130
1241
|
const { scope, state } = await this.resolveState(scopeId);
|
|
1131
1242
|
this.assertNoAmbientSlashCommandTeardown({ storageKey: state.storageKey });
|
|
1132
1243
|
validateIdentifier(sessionId, 'sessionId');
|
|
1133
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> {
|
|
1134
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> {
|
|
1135
1287
|
const existingDeletion = state.sessionDeletions.get(deletionKey);
|
|
1136
1288
|
if (existingDeletion) return existingDeletion;
|
|
1137
1289
|
let deletion!: Promise<void>;
|
|
1138
|
-
deletion =
|
|
1290
|
+
deletion = operation().finally(() => {
|
|
1139
1291
|
if (state.sessionDeletions.get(deletionKey) === deletion) {
|
|
1140
1292
|
state.sessionDeletions.delete(deletionKey);
|
|
1141
1293
|
}
|
|
@@ -1144,6 +1296,60 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1144
1296
|
return deletion;
|
|
1145
1297
|
}
|
|
1146
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
|
+
|
|
1147
1353
|
private async deleteSessionInternal(
|
|
1148
1354
|
state: IStorageState,
|
|
1149
1355
|
scopeId: string,
|
|
@@ -1173,36 +1379,28 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1173
1379
|
return;
|
|
1174
1380
|
}
|
|
1175
1381
|
const stored = this.requireSession(state, sessionId);
|
|
1176
|
-
|
|
1177
|
-
let descendantRoots: string[] = [];
|
|
1178
|
-
await this.mutateScope(state, () => {
|
|
1179
|
-
if (state.sessions.get(sessionId) !== stored) {
|
|
1180
|
-
throw new FlexHarnessNotFoundError('Session', sessionId);
|
|
1181
|
-
}
|
|
1382
|
+
const reservation = await this.mutateScope(state, () => {
|
|
1182
1383
|
const subtree = this.collectSessionSubtree(state, sessionId);
|
|
1183
|
-
descendantRoots = this.descendantTombstoneRoots(state, sessionId);
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
stored: entry,
|
|
1192
|
-
domainsCompleted: false,
|
|
1193
|
-
});
|
|
1194
|
-
state.tombstones.set(entrySessionId, {
|
|
1195
|
-
sessionId: entrySessionId,
|
|
1196
|
-
...requireSessionGeneration(entry.session),
|
|
1197
|
-
deletedAt,
|
|
1198
|
-
rootSessionId: sessionId,
|
|
1199
|
-
depth: (entry.session.depth ?? rootDepth) - rootDepth,
|
|
1200
|
-
...(entry.session.parentSessionId === undefined
|
|
1201
|
-
? {}
|
|
1202
|
-
: { parentSessionId: entry.session.parentSessionId }),
|
|
1203
|
-
});
|
|
1204
|
-
}
|
|
1384
|
+
const descendantRoots = this.descendantTombstoneRoots(state, sessionId);
|
|
1385
|
+
return this.reserveLiveSessionDeletion(
|
|
1386
|
+
state,
|
|
1387
|
+
sessionId,
|
|
1388
|
+
stored,
|
|
1389
|
+
subtree,
|
|
1390
|
+
descendantRoots,
|
|
1391
|
+
);
|
|
1205
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;
|
|
1206
1404
|
const reason = this.trustInternalError(new FlexHarnessAbortError('The session subtree was deleted.'));
|
|
1207
1405
|
const slashCommandErrors: unknown[] = [];
|
|
1208
1406
|
for (const deleted of deletedSessions) {
|
|
@@ -1251,7 +1449,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1251
1449
|
throw this.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-delete');
|
|
1252
1450
|
}
|
|
1253
1451
|
for (const deleted of childFirstDeletedSessions) {
|
|
1254
|
-
this.emitEvent(scopeId, deleted
|
|
1452
|
+
this.emitEvent(scopeId, deleted, {
|
|
1255
1453
|
type: 'session.deleted',
|
|
1256
1454
|
session: publicSnapshot(deleted),
|
|
1257
1455
|
});
|
|
@@ -2416,7 +2614,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2416
2614
|
.find((segment) => segment?.status === 'completed');
|
|
2417
2615
|
if (!target) throw new FlexHarnessValidationError('Pending reversion has no target candidate.');
|
|
2418
2616
|
await this.resumePendingApply(state, stored, scopeId, scope, operationSignal);
|
|
2419
|
-
this.emitEvent(scopeId,
|
|
2617
|
+
this.emitEvent(scopeId, stored.session, {
|
|
2420
2618
|
type: 'session.history.changed',
|
|
2421
2619
|
direction,
|
|
2422
2620
|
runId: target.runId,
|
|
@@ -2467,7 +2665,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2467
2665
|
scope,
|
|
2468
2666
|
operationSignal,
|
|
2469
2667
|
);
|
|
2470
|
-
this.emitEvent(scopeId,
|
|
2668
|
+
this.emitEvent(scopeId, stored.session, {
|
|
2471
2669
|
type: 'session.history.changed',
|
|
2472
2670
|
direction,
|
|
2473
2671
|
runId: unit.target.runId,
|
|
@@ -2765,7 +2963,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2765
2963
|
throw this.projectOperationError(error, 'persistence', scopeId, sessionId, 'archive-horizon');
|
|
2766
2964
|
}
|
|
2767
2965
|
if (branchCommitted) {
|
|
2768
|
-
this.emitEvent(scopeId,
|
|
2966
|
+
this.emitEvent(scopeId, stored.session, {
|
|
2769
2967
|
type: 'session.history.changed',
|
|
2770
2968
|
direction: 'branch',
|
|
2771
2969
|
});
|
|
@@ -3231,14 +3429,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3231
3429
|
}
|
|
3232
3430
|
|
|
3233
3431
|
const runType = run.scheduleKey ? 'run.scheduled' : 'run.started';
|
|
3234
|
-
this.emitEvent(run.scopeId, run.
|
|
3432
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
3235
3433
|
type: runType,
|
|
3236
3434
|
runId: run.runId,
|
|
3237
3435
|
messageId: run.userMessageId,
|
|
3238
3436
|
session: publicSnapshot(run.stored.session),
|
|
3239
3437
|
});
|
|
3240
3438
|
for (const message of [run.reservedUserMessage!, run.reservedAssistantMessage!]) {
|
|
3241
|
-
this.emitEvent(run.scopeId, run.
|
|
3439
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
3242
3440
|
type: 'message.created',
|
|
3243
3441
|
runId: run.runId,
|
|
3244
3442
|
messageId: message.messageId,
|
|
@@ -3307,7 +3505,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3307
3505
|
type: 'prompt.queued' | 'prompt.started' | 'prompt.running' | 'prompt.finished',
|
|
3308
3506
|
entry: IFlexPromptQueueEntry = this.projectPromptQueueEntry(queued),
|
|
3309
3507
|
): void {
|
|
3310
|
-
this.emitEvent(queued.scopeId, queued.
|
|
3508
|
+
this.emitEvent(queued.scopeId, queued.stored.session, {
|
|
3311
3509
|
type,
|
|
3312
3510
|
queueId: queued.queueId,
|
|
3313
3511
|
...(entry.runId ? { runId: entry.runId } : {}),
|
|
@@ -3991,6 +4189,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3991
4189
|
scopeId: run.scopeId,
|
|
3992
4190
|
scope: run.scope as TScope,
|
|
3993
4191
|
sessionId: run.sessionId,
|
|
4192
|
+
...requireSessionGeneration(run.stored.session),
|
|
3994
4193
|
runId: run.runId,
|
|
3995
4194
|
...(options.modelHint ? { modelHint: options.modelHint } : {}),
|
|
3996
4195
|
...resolverRelationship,
|
|
@@ -4190,7 +4389,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4190
4389
|
run.stored.session.updatedAt = new Date().toISOString();
|
|
4191
4390
|
session = publicSnapshot(run.stored.session);
|
|
4192
4391
|
});
|
|
4193
|
-
this.emitEvent(run.scopeId,
|
|
4392
|
+
this.emitEvent(run.scopeId, session, {
|
|
4194
4393
|
type: 'session.updated',
|
|
4195
4394
|
session,
|
|
4196
4395
|
});
|
|
@@ -4722,7 +4921,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4722
4921
|
throw aborted;
|
|
4723
4922
|
}
|
|
4724
4923
|
state.sessions.set(sessionId, loaded);
|
|
4725
|
-
this.emitEvent(run.scopeId,
|
|
4924
|
+
this.emitEvent(run.scopeId, metadata, {
|
|
4726
4925
|
type: 'session.created',
|
|
4727
4926
|
session: publicSnapshot(metadata),
|
|
4728
4927
|
});
|
|
@@ -5531,7 +5730,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5531
5730
|
return;
|
|
5532
5731
|
}
|
|
5533
5732
|
await this.mutateProjection(state, stored, () => this.commitRevertedBranchState(stored), true);
|
|
5534
|
-
this.emitEvent(scopeId, stored.session
|
|
5733
|
+
this.emitEvent(scopeId, stored.session, {
|
|
5535
5734
|
type: 'session.history.changed',
|
|
5536
5735
|
direction: 'branch',
|
|
5537
5736
|
});
|
|
@@ -6057,7 +6256,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6057
6256
|
}
|
|
6058
6257
|
}
|
|
6059
6258
|
for (const message of [terminal.userMessage, terminal.assistantMessage]) {
|
|
6060
|
-
this.emitEvent(run.scopeId, run.
|
|
6259
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
6061
6260
|
type: 'message.updated',
|
|
6062
6261
|
runId: run.runId,
|
|
6063
6262
|
messageId: message.messageId,
|
|
@@ -6075,7 +6274,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6075
6274
|
const status: IFlexRunFinishedEvent['status'] = error
|
|
6076
6275
|
? cancelled ? 'cancelled' : 'failed'
|
|
6077
6276
|
: 'completed';
|
|
6078
|
-
this.emitEvent(run.scopeId, run.
|
|
6277
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
6079
6278
|
type: 'run.finished',
|
|
6080
6279
|
runId: run.runId,
|
|
6081
6280
|
messageId: run.assistantMessageId,
|
|
@@ -6333,6 +6532,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6333
6532
|
permissionId: plugins.crypto.randomUUID(),
|
|
6334
6533
|
scopeId: run.scopeId,
|
|
6335
6534
|
sessionId: run.sessionId,
|
|
6535
|
+
...requireSessionGeneration(run.stored.session),
|
|
6336
6536
|
runId: run.runId,
|
|
6337
6537
|
kind: input.kind,
|
|
6338
6538
|
description: truncateUtf8(input.description, maxTransferMetadataBytes),
|
|
@@ -6381,7 +6581,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6381
6581
|
return promise;
|
|
6382
6582
|
}
|
|
6383
6583
|
if (!pending.settled) {
|
|
6384
|
-
this.emitEvent(run.scopeId, run.
|
|
6584
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
6385
6585
|
type: 'permission.requested',
|
|
6386
6586
|
runId: run.runId,
|
|
6387
6587
|
request: publicSnapshot(request),
|
|
@@ -6482,7 +6682,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6482
6682
|
state.pendingPermissions.delete(pending.request.permissionId);
|
|
6483
6683
|
pending.run.pendingPermissionIds.delete(pending.request.permissionId);
|
|
6484
6684
|
pending.run.controller.signal.removeEventListener('abort', pending.abortListener);
|
|
6485
|
-
this.emitEvent(pending.request.scopeId, pending.
|
|
6685
|
+
this.emitEvent(pending.request.scopeId, pending.run.stored.session, {
|
|
6486
6686
|
type: 'permission.resolved',
|
|
6487
6687
|
runId: pending.request.runId,
|
|
6488
6688
|
request: publicSnapshot(pending.request),
|
|
@@ -7717,93 +7917,112 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7717
7917
|
};
|
|
7718
7918
|
}
|
|
7719
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
|
+
|
|
7720
7932
|
private mutateScope<TValue>(
|
|
7721
7933
|
state: IStorageState,
|
|
7722
7934
|
mutation: () => TValue,
|
|
7723
7935
|
allowDuringDrain = false,
|
|
7724
7936
|
): Promise<TValue> {
|
|
7725
|
-
const operation = state.scopeQueue.then(
|
|
7937
|
+
const operation = state.scopeQueue.then(() => {
|
|
7726
7938
|
if (!allowDuringDrain) this.assertStateAcceptingWork(state);
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
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) {
|
|
7762
7994
|
restore();
|
|
7763
7995
|
throw error;
|
|
7764
7996
|
}
|
|
7765
|
-
|
|
7766
|
-
try {
|
|
7767
|
-
await this.stores.scopes.save(state.storageKey, snapshot, beforeRevision);
|
|
7768
|
-
state.revision = snapshot.revision;
|
|
7769
|
-
return result;
|
|
7770
|
-
} catch (error) {
|
|
7771
|
-
if (error instanceof FlexHarnessStoreConflictError) {
|
|
7772
|
-
restore();
|
|
7773
|
-
throw error;
|
|
7774
|
-
}
|
|
7775
|
-
if (error instanceof FlexHarnessStoreCommitUncertainError) {
|
|
7776
|
-
state.revision = snapshot.revision;
|
|
7777
|
-
state.lifecycle = 'fenced';
|
|
7778
|
-
throw error;
|
|
7779
|
-
}
|
|
7780
|
-
let current: IFlexScopeSnapshot | undefined;
|
|
7781
|
-
let reconciliationError: unknown;
|
|
7782
|
-
try {
|
|
7783
|
-
current = await this.stores.scopes.load(state.storageKey);
|
|
7784
|
-
} catch (loadError) {
|
|
7785
|
-
reconciliationError = loadError;
|
|
7786
|
-
}
|
|
7787
|
-
if (current && JSON.stringify(current) === JSON.stringify(snapshot)) {
|
|
7788
|
-
state.revision = snapshot.revision;
|
|
7789
|
-
throw error;
|
|
7790
|
-
}
|
|
7791
|
-
if (
|
|
7792
|
-
(current === undefined && beforeRevision === 0)
|
|
7793
|
-
|| (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot))
|
|
7794
|
-
) {
|
|
7795
|
-
restore();
|
|
7796
|
-
throw error;
|
|
7797
|
-
}
|
|
7997
|
+
if (error instanceof FlexHarnessStoreCommitUncertainError) {
|
|
7798
7998
|
state.revision = snapshot.revision;
|
|
7799
7999
|
state.lifecycle = 'fenced';
|
|
7800
|
-
throw
|
|
7801
|
-
? error
|
|
7802
|
-
: combineErrors([error, reconciliationError]);
|
|
8000
|
+
throw error;
|
|
7803
8001
|
}
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
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
|
+
}
|
|
7807
8026
|
}
|
|
7808
8027
|
|
|
7809
8028
|
private mutateProjection<TValue>(
|
|
@@ -8237,6 +8456,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8237
8456
|
|| left.sessionId.localeCompare(right.sessionId));
|
|
8238
8457
|
}
|
|
8239
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
|
+
|
|
8240
8467
|
private descendantTombstoneRoots(
|
|
8241
8468
|
state: IStorageState,
|
|
8242
8469
|
ancestorSessionId: string,
|
|
@@ -9417,7 +9644,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9417
9644
|
part: TFlexMessagePart,
|
|
9418
9645
|
): void {
|
|
9419
9646
|
const { messageIndex, partIndex } = this.partEventCoordinates(run, part.partId);
|
|
9420
|
-
this.emitEvent(run.scopeId, run.
|
|
9647
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
9421
9648
|
type,
|
|
9422
9649
|
runId: run.runId,
|
|
9423
9650
|
messageId: run.assistantMessageId,
|
|
@@ -9435,7 +9662,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9435
9662
|
accounting: { baseTextUtf8Bytes: number; textUtf8Bytes: number },
|
|
9436
9663
|
): void {
|
|
9437
9664
|
const { messageIndex, partIndex } = this.partEventCoordinates(run, part.partId);
|
|
9438
|
-
this.emitEvent(run.scopeId, run.
|
|
9665
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
9439
9666
|
type: 'part.delta',
|
|
9440
9667
|
runId: run.runId,
|
|
9441
9668
|
messageId: run.assistantMessageId,
|
|
@@ -9465,14 +9692,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9465
9692
|
return { messageIndex, partIndex };
|
|
9466
9693
|
}
|
|
9467
9694
|
|
|
9468
|
-
private emitEvent(scopeId: string,
|
|
9695
|
+
private emitEvent(scopeId: string, sourceSession: IFlexSession, details: TEventDetails): void {
|
|
9696
|
+
const generation = requireSessionGeneration(sourceSession);
|
|
9469
9697
|
const event = deepFreeze(cloneSerializable({
|
|
9470
9698
|
eventId: plugins.crypto.randomUUID(),
|
|
9471
9699
|
sequence: ++this.sequence,
|
|
9472
9700
|
timestamp: new Date().toISOString(),
|
|
9473
9701
|
scopeId,
|
|
9474
|
-
sessionId,
|
|
9475
9702
|
...details,
|
|
9703
|
+
sessionId: sourceSession.sessionId,
|
|
9704
|
+
...generation,
|
|
9476
9705
|
})) as TFlexHarnessEvent;
|
|
9477
9706
|
for (const listener of this.listeners) {
|
|
9478
9707
|
try {
|