@modelprofile.com/flexharness 10.23.0 → 10.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.flexharness.admission.d.ts +17 -0
  3. package/dist_ts/classes.flexharness.admission.js +263 -0
  4. package/dist_ts/classes.flexharness.callbacks.d.ts +38 -0
  5. package/dist_ts/classes.flexharness.callbacks.js +313 -0
  6. package/dist_ts/classes.flexharness.compaction.d.ts +25 -0
  7. package/dist_ts/classes.flexharness.compaction.js +87 -0
  8. package/dist_ts/classes.flexharness.configurations.d.ts +11 -0
  9. package/dist_ts/classes.flexharness.configurations.js +137 -0
  10. package/dist_ts/classes.flexharness.core.d.ts +143 -0
  11. package/dist_ts/classes.flexharness.core.js +1538 -0
  12. package/dist_ts/classes.flexharness.d.ts +2 -437
  13. package/dist_ts/classes.flexharness.deletion.d.ts +31 -0
  14. package/dist_ts/classes.flexharness.deletion.js +396 -0
  15. package/dist_ts/classes.flexharness.imports.d.ts +24 -0
  16. package/dist_ts/classes.flexharness.imports.js +345 -0
  17. package/dist_ts/classes.flexharness.js +119 -9776
  18. package/dist_ts/classes.flexharness.lifecycle.d.ts +47 -0
  19. package/dist_ts/classes.flexharness.lifecycle.js +625 -0
  20. package/dist_ts/classes.flexharness.permissions.d.ts +19 -0
  21. package/dist_ts/classes.flexharness.permissions.js +450 -0
  22. package/dist_ts/classes.flexharness.projects.d.ts +55 -0
  23. package/dist_ts/classes.flexharness.projects.js +575 -0
  24. package/dist_ts/classes.flexharness.prompts.d.ts +44 -0
  25. package/dist_ts/classes.flexharness.prompts.js +659 -0
  26. package/dist_ts/classes.flexharness.providers.d.ts +18 -0
  27. package/dist_ts/classes.flexharness.providers.js +267 -0
  28. package/dist_ts/classes.flexharness.results.d.ts +26 -0
  29. package/dist_ts/classes.flexharness.results.js +89 -0
  30. package/dist_ts/classes.flexharness.reversions.d.ts +52 -0
  31. package/dist_ts/classes.flexharness.reversions.js +1306 -0
  32. package/dist_ts/classes.flexharness.runs.d.ts +63 -0
  33. package/dist_ts/classes.flexharness.runs.js +1058 -0
  34. package/dist_ts/classes.flexharness.sessions.d.ts +20 -0
  35. package/dist_ts/classes.flexharness.sessions.js +381 -0
  36. package/dist_ts/classes.flexharness.slashcommands.d.ts +40 -0
  37. package/dist_ts/classes.flexharness.slashcommands.js +458 -0
  38. package/dist_ts/classes.flexharness.subagents.d.ts +31 -0
  39. package/dist_ts/classes.flexharness.subagents.js +690 -0
  40. package/dist_ts/classes.flexharness.tooldispatch.d.ts +53 -0
  41. package/dist_ts/classes.flexharness.tooldispatch.js +418 -0
  42. package/dist_ts/interfaces.flexharness.d.ts +455 -0
  43. package/dist_ts/interfaces.flexharness.js +2 -0
  44. package/dist_ts/utils.flexharness.d.ts +19 -0
  45. package/dist_ts/utils.flexharness.js +83 -0
  46. package/dist_ts_compaction/plugins.d.ts +2 -2
  47. package/dist_ts_compaction/plugins.js +3 -3
  48. package/dist_ts_compaction/smartagent.compaction.js +6 -5
  49. package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +9 -0
  50. package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +13 -1
  51. package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +2 -2
  52. package/dist_ts_stores_nosqldb/classes.recordstore.js +3 -3
  53. package/package.json +3 -3
  54. package/readme.md +1 -1
  55. package/ts/00_commitinfo_data.ts +1 -1
  56. package/ts/classes.flexharness.admission.ts +331 -0
  57. package/ts/classes.flexharness.callbacks.ts +352 -0
  58. package/ts/classes.flexharness.compaction.ts +167 -0
  59. package/ts/classes.flexharness.configurations.ts +167 -0
  60. package/ts/classes.flexharness.core.ts +1830 -0
  61. package/ts/classes.flexharness.deletion.ts +616 -0
  62. package/ts/classes.flexharness.imports.ts +414 -0
  63. package/ts/classes.flexharness.lifecycle.ts +786 -0
  64. package/ts/classes.flexharness.permissions.ts +535 -0
  65. package/ts/classes.flexharness.projects.ts +1006 -0
  66. package/ts/classes.flexharness.prompts.ts +857 -0
  67. package/ts/classes.flexharness.providers.ts +322 -0
  68. package/ts/classes.flexharness.results.ts +98 -0
  69. package/ts/classes.flexharness.reversions.ts +1684 -0
  70. package/ts/classes.flexharness.runs.ts +1204 -0
  71. package/ts/classes.flexharness.sessions.ts +483 -0
  72. package/ts/classes.flexharness.slashcommands.ts +705 -0
  73. package/ts/classes.flexharness.subagents.ts +860 -0
  74. package/ts/classes.flexharness.tooldispatch.ts +475 -0
  75. package/ts/classes.flexharness.ts +147 -12567
  76. package/ts/interfaces.flexharness.ts +517 -0
  77. package/ts/readme.md +1 -1
  78. package/ts/utils.flexharness.ts +121 -0
  79. package/ts_compaction/plugins.ts +2 -2
  80. package/ts_compaction/smartagent.compaction.ts +5 -4
  81. package/ts_stores_nosqldb/classes.nosqldbstores.ts +13 -0
  82. package/ts_stores_nosqldb/classes.recordstore.ts +5 -5
  83. package/ts_stores_nosqldb/readme.md +7 -0
@@ -0,0 +1,535 @@
1
+ import * as plugins from './plugins.js';
2
+ import {
3
+ FlexHarnessAbortError,
4
+ FlexHarnessNotFoundError,
5
+ FlexHarnessPermissionRejectedError,
6
+ FlexHarnessPermissionStateError,
7
+ FlexHarnessStoreConflictError,
8
+ FlexHarnessStoreCommitUncertainError,
9
+ FlexHarnessValidationError,
10
+ } from './errors.js';
11
+ import type {
12
+ IFlexAgentContextInvocation,
13
+ IFlexPermissionRequest,
14
+ IFlexPermissionRequestInput,
15
+ IFlexPermissionSnapshot,
16
+ IFlexSessionPermissionPolicyContext,
17
+ TFlexPermissionDecision,
18
+ TJsonValue,
19
+ } from './interfaces.js';
20
+ import { cloneSerializable, normalizeJsonValue } from './utils.json.js';
21
+ import { maxTransferMetadataBytes, publicSnapshot, truncateUtf8 } from './utils.transfer.js';
22
+ import { validateIdentifier } from './utils.validation.js';
23
+ import type {
24
+ IStoredSessionState,
25
+ IStorageState,
26
+ IActiveRun,
27
+ IPendingPermission,
28
+ } from './interfaces.flexharness.js';
29
+ import { requireSessionGeneration, combineErrors } from './utils.flexharness.js';
30
+ import type { FlexHarnessCore } from './classes.flexharness.core.js';
31
+
32
+ /** Permission requests and responses, and the permission policy of sessions. */
33
+ export class FlexHarnessPermissions<TScope> {
34
+ constructor(private readonly core: FlexHarnessCore<TScope>) {}
35
+
36
+ public async listPendingPermissions(
37
+ scopeId: string,
38
+ sessionId?: string,
39
+ ): Promise<IFlexPermissionRequest[]> {
40
+ const { state } = await this.core.resolveState(scopeId);
41
+ await state.scopeQueue;
42
+ this.core.assertStateAcceptingWork(state);
43
+ return publicSnapshot([...state.pendingPermissions.values()]
44
+ .map((pending) => pending.request)
45
+ .filter((request) => !sessionId || request.sessionId === sessionId)
46
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt)));
47
+ }
48
+
49
+ public async respondToPermission(
50
+ scopeId: string,
51
+ sessionId: string,
52
+ permissionId: string,
53
+ decision: TFlexPermissionDecision,
54
+ ): Promise<void> {
55
+ validateIdentifier(permissionId, 'permissionId');
56
+ const { scope, state } = await this.core.resolveState(scopeId);
57
+ const pending = state.pendingPermissions.get(permissionId);
58
+ if (!pending || pending.request.sessionId !== sessionId) {
59
+ throw new FlexHarnessNotFoundError('Permission', permissionId);
60
+ }
61
+ const context = this.core.compaction.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId);
62
+ const response = pending.responseQueue.then(() => this.core.compaction.withCompactorContext(
63
+ context,
64
+ () => this.applyPermissionResponse(state, pending, decision, context),
65
+ ));
66
+ pending.responseQueue = response.then(() => undefined, () => undefined);
67
+ return response;
68
+ }
69
+
70
+ public async requestPermission(
71
+ state: IStorageState,
72
+ run: IActiveRun,
73
+ input: IFlexPermissionRequestInput,
74
+ exactMetadata?: TJsonValue,
75
+ ): Promise<void> {
76
+ validateIdentifier(input.kind, 'permission kind');
77
+ validateIdentifier(input.description, 'permission description');
78
+ if (input.rememberKey !== undefined) validateIdentifier(input.rememberKey, 'rememberKey');
79
+ if (input.toolCallId !== undefined) validateIdentifier(input.toolCallId, 'toolCallId');
80
+ run.controller.signal.throwIfAborted();
81
+ await run.stored.permissionQueue;
82
+ run.controller.signal.throwIfAborted();
83
+ if (state.activeRuns.get(run.sessionId) !== run || run.callbacksClosed) {
84
+ throw new FlexHarnessPermissionStateError(`Run "${run.runId}" no longer accepts permission requests.`);
85
+ }
86
+ if (!run.permissionPolicy && input.rememberKey && run.stored.rememberedPermissionKeys.has(input.rememberKey)) {
87
+ return;
88
+ }
89
+ const candidate: IFlexPermissionRequest = {
90
+ permissionId: plugins.crypto.randomUUID(),
91
+ scopeId: run.scopeId,
92
+ sessionId: run.sessionId,
93
+ ...requireSessionGeneration(run.stored.session),
94
+ runId: run.runId,
95
+ kind: input.kind,
96
+ description: truncateUtf8(input.description, maxTransferMetadataBytes),
97
+ ...(input.toolCallId ? { toolCallId: input.toolCallId } : {}),
98
+ ...(input.rememberKey ? { rememberKey: input.rememberKey } : {}),
99
+ ...(exactMetadata !== undefined
100
+ ? { metadata: cloneSerializable(exactMetadata) }
101
+ : input.metadata === undefined
102
+ ? {}
103
+ : { metadata: normalizeJsonValue(input.metadata, this.core.toolOutputLimits) }),
104
+ createdAt: new Date().toISOString(),
105
+ };
106
+ const policyDecision = run.permissionPolicy
107
+ ? await this.decideSessionPermissionPolicy(state, run, candidate)
108
+ : 'inherit';
109
+ if (policyDecision === 'allow' || policyDecision === 'deny') {
110
+ this.core.emitEvent(run.scopeId, run.stored.session, {
111
+ type: 'permission.resolved',
112
+ runId: run.runId,
113
+ request: publicSnapshot(candidate),
114
+ decision: policyDecision === 'allow' ? 'once' : 'reject',
115
+ origin: 'policy',
116
+ });
117
+ if (policyDecision === 'deny') {
118
+ const rejection = this.core.trustInternalError(
119
+ new FlexHarnessPermissionRejectedError(candidate.permissionId),
120
+ );
121
+ this.core.runs.abortRunInternally(run, rejection);
122
+ throw rejection;
123
+ }
124
+ return;
125
+ }
126
+ if (policyDecision === 'inherit'
127
+ && input.rememberKey && run.stored.rememberedPermissionKeys.has(input.rememberKey)) return;
128
+ const request: IFlexPermissionRequest = { ...candidate };
129
+ if (policyDecision === 'ask') {
130
+ delete request.rememberKey;
131
+ request.requiresExplicitResponse = true;
132
+ }
133
+ let resolvePermission!: () => void;
134
+ let rejectPermission!: (error: unknown) => void;
135
+ const promise = new Promise<void>((resolve, reject) => {
136
+ resolvePermission = resolve;
137
+ rejectPermission = reject;
138
+ });
139
+ void promise.catch(() => undefined);
140
+ const abortListener = () => {
141
+ const pending = state.pendingPermissions.get(request.permissionId);
142
+ if (pending) this.rejectPending(state, pending, run.controller.signal.reason);
143
+ };
144
+ const pending: IPendingPermission = {
145
+ request,
146
+ run,
147
+ resolve: resolvePermission,
148
+ reject: rejectPermission,
149
+ settled: false,
150
+ responding: false,
151
+ responseQueue: Promise.resolve(),
152
+ abortListener,
153
+ };
154
+ const closePublication = this.core.openPublication(state, run.sessionId);
155
+ try {
156
+ state.pendingPermissions.set(request.permissionId, pending);
157
+ run.pendingPermissionIds.add(request.permissionId);
158
+ run.controller.signal.addEventListener('abort', abortListener, { once: true });
159
+ try {
160
+ await this.core.mutateScope(state, () => {
161
+ if (state.sessions.get(run.sessionId) !== run.stored) {
162
+ throw new FlexHarnessAbortError('The session was deleted while permission was pending.');
163
+ }
164
+ run.stored.session.status = 'waiting_permission';
165
+ run.stored.session.activity.status = 'waiting_permission';
166
+ run.stored.session.updatedAt = new Date().toISOString();
167
+ }, true);
168
+ } catch (error) {
169
+ this.forceRejectPending(state, pending, this.core.projectExternalError(run, error, 'persistence'));
170
+ return promise;
171
+ }
172
+ if (!pending.settled) {
173
+ this.core.emitEvent(run.scopeId, run.stored.session, {
174
+ type: 'permission.requested',
175
+ runId: run.runId,
176
+ request: publicSnapshot(request),
177
+ });
178
+ }
179
+ } finally {
180
+ closePublication();
181
+ }
182
+ return promise;
183
+ }
184
+
185
+ private async decideSessionPermissionPolicy(
186
+ state: IStorageState,
187
+ run: IActiveRun,
188
+ request: IFlexPermissionRequest,
189
+ ): Promise<'inherit' | 'allow' | 'ask' | 'deny'> {
190
+ const policy = run.permissionPolicy!;
191
+ const signal = run.controller.signal;
192
+ const context: Readonly<IFlexSessionPermissionPolicyContext<unknown>> = Object.freeze({
193
+ scopeId: run.scopeId,
194
+ scope: run.scope,
195
+ storageKey: state.storageKey,
196
+ sessionId: run.sessionId,
197
+ ...requireSessionGeneration(run.stored.session),
198
+ runId: run.runId,
199
+ configurationRef: run.stored.session.configurationRef!,
200
+ ...(run.stored.session.agent === undefined ? {} : { agent: run.stored.session.agent }),
201
+ request: publicSnapshot(request),
202
+ signal,
203
+ });
204
+ let onAbort!: () => void;
205
+ const aborted = new Promise<never>((_resolve, reject) => {
206
+ onAbort = () => reject(signal.reason ?? this.core.trustInternalError(new FlexHarnessAbortError()));
207
+ });
208
+ signal.addEventListener('abort', onAbort, { once: true });
209
+ if (signal.aborted) onAbort();
210
+ let decision: unknown;
211
+ try {
212
+ decision = await Promise.race([
213
+ Promise.resolve().then(() => {
214
+ signal.throwIfAborted();
215
+ return policy(context);
216
+ }),
217
+ aborted,
218
+ ]);
219
+ if (!['inherit', 'allow', 'ask', 'deny'].includes(decision as string)) {
220
+ throw new FlexHarnessValidationError('Session permissionPolicy returned an invalid decision.');
221
+ }
222
+ } catch (error) {
223
+ if (signal.aborted) throw signal.reason;
224
+ const projected = this.core.projectExternalError(run, error, 'permissionPolicy');
225
+ this.core.runs.abortRunInternally(run, projected);
226
+ throw projected;
227
+ } finally {
228
+ signal.removeEventListener('abort', onAbort);
229
+ }
230
+ signal.throwIfAborted();
231
+ const generation = requireSessionGeneration(run.stored.session);
232
+ if (
233
+ state.lifecycle !== 'active'
234
+ || state.activeRuns.get(run.sessionId) !== run
235
+ || state.sessions.get(run.sessionId) !== run.stored
236
+ || run.callbacksClosed
237
+ || generation.sessionGenerationId !== request.sessionGenerationId
238
+ || generation.sessionGenerationSequence !== request.sessionGenerationSequence
239
+ ) {
240
+ const ownershipError = this.core.trustInternalError(new FlexHarnessAbortError(
241
+ 'Permission policy lost its exact run ownership.',
242
+ ));
243
+ this.core.runs.abortRunInternally(run, ownershipError);
244
+ throw ownershipError;
245
+ }
246
+ return decision as 'inherit' | 'allow' | 'ask' | 'deny';
247
+ }
248
+
249
+ private async applyPermissionResponse(
250
+ state: IStorageState,
251
+ pending: IPendingPermission,
252
+ decision: TFlexPermissionDecision,
253
+ context: IFlexAgentContextInvocation<TScope>,
254
+ ): Promise<void> {
255
+ if (pending.settled) {
256
+ throw new FlexHarnessPermissionStateError(`Permission "${pending.request.permissionId}" has already been resolved.`);
257
+ }
258
+ if (!['once', 'always', 'reject'].includes(decision)) {
259
+ throw new FlexHarnessPermissionStateError(`Unknown permission decision "${decision}".`);
260
+ }
261
+ if (decision === 'always' && !pending.request.rememberKey) {
262
+ throw new FlexHarnessPermissionStateError('The always decision requires rememberKey.');
263
+ }
264
+ const stored = pending.run.stored;
265
+ const rememberKey = pending.request.rememberKey;
266
+ let addedRememberKey = false;
267
+ pending.responding = true;
268
+ try {
269
+ if (decision === 'always' && rememberKey) {
270
+ await this.mutatePermissions(state, stored, () => {
271
+ addedRememberKey = !stored.rememberedPermissionKeys.has(rememberKey);
272
+ stored.rememberedPermissionKeys.add(rememberKey);
273
+ });
274
+ }
275
+ await this.core.mutateScope(state, () => {
276
+ if (state.sessions.get(pending.request.sessionId) !== stored) {
277
+ throw new FlexHarnessAbortError('The session was deleted while permission was resolving.');
278
+ }
279
+ const hasOtherPending = [...state.pendingPermissions.values()].some((entry) =>
280
+ entry !== pending && !entry.settled && entry.request.sessionId === pending.request.sessionId);
281
+ stored.session.status = hasOtherPending ? 'waiting_permission' : 'running';
282
+ stored.session.activity.status = hasOtherPending ? 'waiting_permission' : 'running';
283
+ stored.session.updatedAt = new Date().toISOString();
284
+ }, true);
285
+ } catch (error) {
286
+ pending.responding = false;
287
+ const projected = this.core.projectExternalError(pending.run, error, 'persistence');
288
+ const rollbackErrors = await this.rollbackRememberedGrant(
289
+ state,
290
+ pending,
291
+ addedRememberKey,
292
+ rememberKey,
293
+ );
294
+ if (
295
+ pending.abortReason === undefined
296
+ && state.sessions.get(pending.request.sessionId) === stored
297
+ && stored.session.status !== 'waiting_permission'
298
+ ) {
299
+ try {
300
+ await this.core.mutateScope(state, () => {
301
+ stored.session.status = 'waiting_permission';
302
+ stored.session.activity.status = 'waiting_permission';
303
+ stored.session.updatedAt = new Date().toISOString();
304
+ }, true);
305
+ } catch (rollbackError) {
306
+ rollbackErrors.push(this.core.projectExternalError(pending.run, rollbackError, 'persistence'));
307
+ }
308
+ }
309
+ if (pending.abortReason !== undefined) {
310
+ this.forceRejectPending(state, pending, pending.abortReason);
311
+ }
312
+ if (rollbackErrors.length > 0 || state.lifecycle === 'fenced') {
313
+ if (!pending.settled) this.forceRejectPending(state, pending, projected);
314
+ const combined = combineErrors([
315
+ ...(pending.abortReason === undefined ? [] : [pending.abortReason]),
316
+ projected,
317
+ ...rollbackErrors,
318
+ ]);
319
+ await this.core.lifecycle.fenceNamespace(state, pending.run, combined);
320
+ throw combined;
321
+ }
322
+ if (pending.abortReason !== undefined) throw pending.abortReason;
323
+ throw projected;
324
+ }
325
+ if (pending.abortReason !== undefined) {
326
+ const rollbackErrors = await this.rollbackRememberedGrant(state, pending, addedRememberKey, rememberKey);
327
+ pending.responding = false;
328
+ this.forceRejectPending(state, pending, pending.abortReason);
329
+ if (rollbackErrors.length > 0) {
330
+ const combined = combineErrors([pending.abortReason, ...rollbackErrors]);
331
+ await this.core.lifecycle.fenceNamespace(state, pending.run, combined);
332
+ throw combined;
333
+ }
334
+ throw pending.abortReason;
335
+ }
336
+ pending.settled = true;
337
+ pending.responding = false;
338
+ state.pendingPermissions.delete(pending.request.permissionId);
339
+ pending.run.pendingPermissionIds.delete(pending.request.permissionId);
340
+ pending.run.controller.signal.removeEventListener('abort', pending.abortListener);
341
+ this.core.emitEvent(pending.request.scopeId, pending.run.stored.session, {
342
+ type: 'permission.resolved',
343
+ runId: pending.request.runId,
344
+ request: publicSnapshot(pending.request),
345
+ decision,
346
+ });
347
+ if (decision === 'reject') {
348
+ const rejection = this.core.trustInternalError(
349
+ new FlexHarnessPermissionRejectedError(pending.request.permissionId),
350
+ );
351
+ this.core.runs.abortRunInternally(pending.run, rejection, context);
352
+ pending.reject(rejection);
353
+ } else {
354
+ pending.resolve();
355
+ }
356
+ }
357
+
358
+ private async rollbackRememberedGrant(
359
+ state: IStorageState,
360
+ pending: IPendingPermission,
361
+ added: boolean,
362
+ rememberKey?: string,
363
+ ): Promise<unknown[]> {
364
+ if (
365
+ !added
366
+ || !rememberKey
367
+ || !pending.run.stored.rememberedPermissionKeys.has(rememberKey)
368
+ ) return [];
369
+ try {
370
+ await this.mutatePermissions(state, pending.run.stored, () => {
371
+ pending.run.stored.rememberedPermissionKeys.delete(rememberKey);
372
+ }, true);
373
+ return [];
374
+ } catch (error) {
375
+ const initialError = this.core.projectExternalError(pending.run, error, 'persistence');
376
+ try {
377
+ await this.reconcileRememberedPermissionRemoval(
378
+ pending.run.stored,
379
+ state.storageKey,
380
+ rememberKey,
381
+ );
382
+ return [];
383
+ } catch (recoveryError) {
384
+ return [initialError, this.core.projectExternalError(pending.run, recoveryError, 'persistence')];
385
+ }
386
+ }
387
+ }
388
+
389
+ private async reconcileRememberedPermissionRemoval(
390
+ stored: IStoredSessionState,
391
+ storageKey: string,
392
+ rememberKey: string,
393
+ ): Promise<void> {
394
+ const operation = stored.permissionQueue.then(async () => {
395
+ let lastError: unknown;
396
+ for (let attempt = 0; attempt < 3; attempt++) {
397
+ try {
398
+ const current = await this.core.stores.permissions.load(storageKey, stored.session.sessionId);
399
+ if (!current || !current.rememberedPermissionKeys.includes(rememberKey)) {
400
+ stored.rememberedPermissionKeys = new Set(current?.rememberedPermissionKeys ?? []);
401
+ stored.permissionRevision = current?.revision ?? 0;
402
+ return;
403
+ }
404
+ const snapshot = {
405
+ ...current,
406
+ revision: current.revision + 1,
407
+ rememberedPermissionKeys: current.rememberedPermissionKeys.filter(
408
+ (entry) => entry !== rememberKey,
409
+ ),
410
+ };
411
+ await this.core.stores.permissions.save(
412
+ storageKey,
413
+ stored.session.sessionId,
414
+ snapshot,
415
+ current.revision,
416
+ );
417
+ stored.rememberedPermissionKeys = new Set(snapshot.rememberedPermissionKeys);
418
+ stored.permissionRevision = snapshot.revision;
419
+ return;
420
+ } catch (error) {
421
+ lastError = error;
422
+ }
423
+ }
424
+ throw lastError ?? new Error('Remembered permission rollback could not be reconciled.');
425
+ });
426
+ stored.permissionQueue = operation.then(() => undefined, () => undefined);
427
+ await operation;
428
+ }
429
+
430
+ public rejectRunPermissions(state: IStorageState, run: IActiveRun, error: unknown): void {
431
+ for (const permissionId of [...run.pendingPermissionIds]) {
432
+ const pending = state.pendingPermissions.get(permissionId);
433
+ if (pending) this.rejectPending(state, pending, error);
434
+ }
435
+ }
436
+
437
+ public rejectPending(state: IStorageState, pending: IPendingPermission, error: unknown): void {
438
+ if (pending.settled) return;
439
+ if (pending.responding) {
440
+ pending.abortReason ??= error;
441
+ return;
442
+ }
443
+ this.forceRejectPending(state, pending, error);
444
+ }
445
+
446
+ private forceRejectPending(state: IStorageState, pending: IPendingPermission, error: unknown): void {
447
+ if (pending.settled) return;
448
+ pending.settled = true;
449
+ state.pendingPermissions.delete(pending.request.permissionId);
450
+ pending.run.pendingPermissionIds.delete(pending.request.permissionId);
451
+ pending.run.controller.signal.removeEventListener('abort', pending.abortListener);
452
+ pending.reject(error);
453
+ }
454
+
455
+ private mutatePermissions<TValue>(
456
+ state: IStorageState,
457
+ stored: IStoredSessionState,
458
+ mutation: () => TValue,
459
+ _allowDuringDrain = false,
460
+ ): Promise<TValue> {
461
+ const operation = stored.permissionQueue.then(async () => {
462
+ const beforeRevision = stored.permissionRevision;
463
+ const beforeKeys = new Set(stored.rememberedPermissionKeys);
464
+ const beforeSnapshot = {
465
+ schemaVersion: 1 as const,
466
+ revision: beforeRevision,
467
+ rememberedPermissionKeys: [...beforeKeys].sort(),
468
+ };
469
+ const restore = () => {
470
+ stored.permissionRevision = beforeRevision;
471
+ stored.rememberedPermissionKeys = beforeKeys;
472
+ };
473
+ let result: TValue;
474
+ try {
475
+ result = mutation();
476
+ } catch (error) {
477
+ restore();
478
+ throw error;
479
+ }
480
+ const snapshot = {
481
+ schemaVersion: 1 as const,
482
+ revision: beforeRevision + 1,
483
+ rememberedPermissionKeys: [...stored.rememberedPermissionKeys].sort(),
484
+ };
485
+ try {
486
+ await this.core.stores.permissions.save(
487
+ state.storageKey,
488
+ stored.session.sessionId,
489
+ snapshot,
490
+ beforeRevision,
491
+ );
492
+ stored.permissionRevision = snapshot.revision;
493
+ return result;
494
+ } catch (error) {
495
+ if (error instanceof FlexHarnessStoreConflictError) {
496
+ restore();
497
+ throw error;
498
+ }
499
+ if (error instanceof FlexHarnessStoreCommitUncertainError) {
500
+ stored.permissionRevision = snapshot.revision;
501
+ state.lifecycle = 'fenced';
502
+ throw error;
503
+ }
504
+ let current: IFlexPermissionSnapshot | undefined;
505
+ let reconciliationError: unknown;
506
+ try {
507
+ current = await this.core.stores.permissions.load(
508
+ state.storageKey,
509
+ stored.session.sessionId,
510
+ );
511
+ } catch (loadError) {
512
+ reconciliationError = loadError;
513
+ }
514
+ if (current && JSON.stringify(current) === JSON.stringify(snapshot)) {
515
+ stored.permissionRevision = snapshot.revision;
516
+ throw error;
517
+ }
518
+ if (
519
+ (current === undefined && beforeRevision === 0)
520
+ || (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot))
521
+ ) {
522
+ restore();
523
+ throw error;
524
+ }
525
+ stored.permissionRevision = snapshot.revision;
526
+ state.lifecycle = 'fenced';
527
+ throw reconciliationError === undefined
528
+ ? error
529
+ : combineErrors([error, reconciliationError]);
530
+ }
531
+ });
532
+ stored.permissionQueue = operation.then(() => undefined, () => undefined);
533
+ return operation;
534
+ }
535
+ }