@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.admission.d.ts +17 -0
- package/dist_ts/classes.flexharness.admission.js +263 -0
- package/dist_ts/classes.flexharness.callbacks.d.ts +38 -0
- package/dist_ts/classes.flexharness.callbacks.js +313 -0
- package/dist_ts/classes.flexharness.compaction.d.ts +25 -0
- package/dist_ts/classes.flexharness.compaction.js +87 -0
- package/dist_ts/classes.flexharness.configurations.d.ts +11 -0
- package/dist_ts/classes.flexharness.configurations.js +137 -0
- package/dist_ts/classes.flexharness.core.d.ts +143 -0
- package/dist_ts/classes.flexharness.core.js +1538 -0
- package/dist_ts/classes.flexharness.d.ts +2 -437
- package/dist_ts/classes.flexharness.deletion.d.ts +31 -0
- package/dist_ts/classes.flexharness.deletion.js +396 -0
- package/dist_ts/classes.flexharness.imports.d.ts +24 -0
- package/dist_ts/classes.flexharness.imports.js +345 -0
- package/dist_ts/classes.flexharness.js +119 -9776
- package/dist_ts/classes.flexharness.lifecycle.d.ts +47 -0
- package/dist_ts/classes.flexharness.lifecycle.js +625 -0
- package/dist_ts/classes.flexharness.permissions.d.ts +19 -0
- package/dist_ts/classes.flexharness.permissions.js +450 -0
- package/dist_ts/classes.flexharness.projects.d.ts +55 -0
- package/dist_ts/classes.flexharness.projects.js +575 -0
- package/dist_ts/classes.flexharness.prompts.d.ts +44 -0
- package/dist_ts/classes.flexharness.prompts.js +659 -0
- package/dist_ts/classes.flexharness.providers.d.ts +18 -0
- package/dist_ts/classes.flexharness.providers.js +267 -0
- package/dist_ts/classes.flexharness.results.d.ts +26 -0
- package/dist_ts/classes.flexharness.results.js +89 -0
- package/dist_ts/classes.flexharness.reversions.d.ts +52 -0
- package/dist_ts/classes.flexharness.reversions.js +1306 -0
- package/dist_ts/classes.flexharness.runs.d.ts +63 -0
- package/dist_ts/classes.flexharness.runs.js +1058 -0
- package/dist_ts/classes.flexharness.sessions.d.ts +20 -0
- package/dist_ts/classes.flexharness.sessions.js +381 -0
- package/dist_ts/classes.flexharness.slashcommands.d.ts +40 -0
- package/dist_ts/classes.flexharness.slashcommands.js +458 -0
- package/dist_ts/classes.flexharness.subagents.d.ts +31 -0
- package/dist_ts/classes.flexharness.subagents.js +690 -0
- package/dist_ts/classes.flexharness.tooldispatch.d.ts +53 -0
- package/dist_ts/classes.flexharness.tooldispatch.js +418 -0
- package/dist_ts/interfaces.flexharness.d.ts +455 -0
- package/dist_ts/interfaces.flexharness.js +2 -0
- package/dist_ts/utils.flexharness.d.ts +19 -0
- package/dist_ts/utils.flexharness.js +83 -0
- package/dist_ts_compaction/plugins.d.ts +2 -2
- package/dist_ts_compaction/plugins.js +3 -3
- package/dist_ts_compaction/smartagent.compaction.js +6 -5
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +9 -0
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +13 -1
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +2 -2
- package/dist_ts_stores_nosqldb/classes.recordstore.js +3 -3
- package/package.json +3 -3
- package/readme.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.admission.ts +331 -0
- package/ts/classes.flexharness.callbacks.ts +352 -0
- package/ts/classes.flexharness.compaction.ts +167 -0
- package/ts/classes.flexharness.configurations.ts +167 -0
- package/ts/classes.flexharness.core.ts +1830 -0
- package/ts/classes.flexharness.deletion.ts +616 -0
- package/ts/classes.flexharness.imports.ts +414 -0
- package/ts/classes.flexharness.lifecycle.ts +786 -0
- package/ts/classes.flexharness.permissions.ts +535 -0
- package/ts/classes.flexharness.projects.ts +1006 -0
- package/ts/classes.flexharness.prompts.ts +857 -0
- package/ts/classes.flexharness.providers.ts +322 -0
- package/ts/classes.flexharness.results.ts +98 -0
- package/ts/classes.flexharness.reversions.ts +1684 -0
- package/ts/classes.flexharness.runs.ts +1204 -0
- package/ts/classes.flexharness.sessions.ts +483 -0
- package/ts/classes.flexharness.slashcommands.ts +705 -0
- package/ts/classes.flexharness.subagents.ts +860 -0
- package/ts/classes.flexharness.tooldispatch.ts +475 -0
- package/ts/classes.flexharness.ts +147 -12567
- package/ts/interfaces.flexharness.ts +517 -0
- package/ts/readme.md +1 -1
- package/ts/utils.flexharness.ts +121 -0
- package/ts_compaction/plugins.ts +2 -2
- package/ts_compaction/smartagent.compaction.ts +5 -4
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +13 -0
- package/ts_stores_nosqldb/classes.recordstore.ts +5 -5
- package/ts_stores_nosqldb/readme.md +7 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FlexHarnessAbortError,
|
|
3
|
+
FlexHarnessNotFoundError,
|
|
4
|
+
FlexHarnessValidationError,
|
|
5
|
+
} from './errors.js';
|
|
6
|
+
import type {
|
|
7
|
+
IFlexAgentContextInvocation,
|
|
8
|
+
IFlexDeleteSessionGenerationCohortInput,
|
|
9
|
+
IFlexDeleteSessionGenerationCohortResult,
|
|
10
|
+
IFlexSession,
|
|
11
|
+
IFlexSessionGeneration,
|
|
12
|
+
IFlexSessionGenerationCohortEntry,
|
|
13
|
+
IFlexSessionTombstone,
|
|
14
|
+
} from './interfaces.js';
|
|
15
|
+
import { cloneSerializable } from './utils.json.js';
|
|
16
|
+
import { publicSnapshot, requireTransferIdentifier } from './utils.transfer.js';
|
|
17
|
+
import {
|
|
18
|
+
validateDeleteSessionGenerationCohortInput,
|
|
19
|
+
validateIdentifier,
|
|
20
|
+
} from './utils.validation.js';
|
|
21
|
+
import type {
|
|
22
|
+
IStoredSessionState,
|
|
23
|
+
IRetainedSessionCleanup,
|
|
24
|
+
IStorageState,
|
|
25
|
+
ISessionDeletionPlan,
|
|
26
|
+
IReservedSessionDeletion,
|
|
27
|
+
} from './interfaces.flexharness.js';
|
|
28
|
+
import { requireSessionGeneration, combineErrors } from './utils.flexharness.js';
|
|
29
|
+
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
30
|
+
|
|
31
|
+
function matchesSessionGeneration(
|
|
32
|
+
session: Pick<
|
|
33
|
+
IFlexSession | IFlexSessionTombstone,
|
|
34
|
+
'sessionGenerationId' | 'sessionGenerationSequence'
|
|
35
|
+
>,
|
|
36
|
+
expected: Readonly<IFlexSessionGeneration>,
|
|
37
|
+
): boolean {
|
|
38
|
+
const actual = requireSessionGeneration(session);
|
|
39
|
+
return actual.sessionGenerationId === expected.sessionGenerationId
|
|
40
|
+
&& actual.sessionGenerationSequence === expected.sessionGenerationSequence;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Session deletion: reservations, subtree tombstones and the cleanup of every domain a session
|
|
45
|
+
* owns.
|
|
46
|
+
*/
|
|
47
|
+
export class FlexHarnessSessionDeletion<TScope> {
|
|
48
|
+
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
49
|
+
|
|
50
|
+
public async deleteSessionGenerationCohort(
|
|
51
|
+
scopeId: string,
|
|
52
|
+
input: IFlexDeleteSessionGenerationCohortInput,
|
|
53
|
+
): Promise<IFlexDeleteSessionGenerationCohortResult> {
|
|
54
|
+
const validated = validateDeleteSessionGenerationCohortInput(input);
|
|
55
|
+
requireTransferIdentifier(validated.root.sessionId, 'root.sessionId');
|
|
56
|
+
for (const entry of validated.authorizedBySessionId.values()) {
|
|
57
|
+
requireTransferIdentifier(entry.sessionId, 'authorizedCohort sessionId');
|
|
58
|
+
}
|
|
59
|
+
this.core.commands.assertNoAmbientSlashCommandTeardown({
|
|
60
|
+
scopeId,
|
|
61
|
+
sessionId: validated.root.sessionId,
|
|
62
|
+
});
|
|
63
|
+
const { scope, state } = await this.core.resolveState(scopeId);
|
|
64
|
+
this.core.commands.assertNoAmbientSlashCommandTeardown({ storageKey: state.storageKey });
|
|
65
|
+
const plan = await this.core.withScopeMutationOwnership(state, async (): Promise<ISessionDeletionPlan> => {
|
|
66
|
+
const stored = state.sessions.get(validated.root.sessionId);
|
|
67
|
+
if (stored) {
|
|
68
|
+
if (!matchesSessionGeneration(stored.session, validated.root)) {
|
|
69
|
+
return { matched: false };
|
|
70
|
+
}
|
|
71
|
+
const subtree = this.collectSessionSubtree(state, validated.root.sessionId);
|
|
72
|
+
const descendantRoots = this.descendantTombstoneRoots(
|
|
73
|
+
state,
|
|
74
|
+
validated.root.sessionId,
|
|
75
|
+
);
|
|
76
|
+
this.assertAuthorizedSessionGenerationCohort(
|
|
77
|
+
[
|
|
78
|
+
...subtree.map((entry) => entry.session),
|
|
79
|
+
...this.tombstonesForRoots(state, descendantRoots),
|
|
80
|
+
],
|
|
81
|
+
validated.authorizedBySessionId,
|
|
82
|
+
);
|
|
83
|
+
const reservation = await this.core.mutateScopeOwned(state, () =>
|
|
84
|
+
this.reserveLiveSessionDeletion(
|
|
85
|
+
state,
|
|
86
|
+
validated.root.sessionId,
|
|
87
|
+
stored,
|
|
88
|
+
subtree,
|
|
89
|
+
descendantRoots,
|
|
90
|
+
));
|
|
91
|
+
return {
|
|
92
|
+
matched: true,
|
|
93
|
+
deletion: this.startReservedSessionDeletion(
|
|
94
|
+
state,
|
|
95
|
+
scopeId,
|
|
96
|
+
scope.scope,
|
|
97
|
+
validated.root.sessionId,
|
|
98
|
+
reservation,
|
|
99
|
+
),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const tombstone = state.tombstones.get(validated.root.sessionId);
|
|
103
|
+
if (!tombstone || !matchesSessionGeneration(tombstone, validated.root)) {
|
|
104
|
+
return { matched: false };
|
|
105
|
+
}
|
|
106
|
+
this.core.imports.assertNoCommittingSessionImport(state, validated.root.sessionId);
|
|
107
|
+
const rootSessionId = tombstone.rootSessionId ?? tombstone.sessionId;
|
|
108
|
+
const descendantRoots = this.descendantTombstoneRoots(state, rootSessionId);
|
|
109
|
+
this.assertAuthorizedSessionGenerationCohort(
|
|
110
|
+
this.tombstonesForRoots(state, [rootSessionId, ...descendantRoots]),
|
|
111
|
+
validated.authorizedBySessionId,
|
|
112
|
+
);
|
|
113
|
+
return {
|
|
114
|
+
matched: true,
|
|
115
|
+
deletion: this.startSessionDeletion(
|
|
116
|
+
state,
|
|
117
|
+
scopeId,
|
|
118
|
+
scope.scope,
|
|
119
|
+
validated.root.sessionId,
|
|
120
|
+
),
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
if (!plan.matched) return publicSnapshot({ matched: false });
|
|
124
|
+
if (!plan.deletion) throw new Error('Matched session deletion has no cleanup operation.');
|
|
125
|
+
await plan.deletion;
|
|
126
|
+
return publicSnapshot({ matched: true });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public async deleteSession(scopeId: string, sessionId: string): Promise<void> {
|
|
130
|
+
this.core.commands.assertNoAmbientSlashCommandTeardown({ scopeId, sessionId });
|
|
131
|
+
const { scope, state } = await this.core.resolveState(scopeId);
|
|
132
|
+
this.core.commands.assertNoAmbientSlashCommandTeardown({ storageKey: state.storageKey });
|
|
133
|
+
validateIdentifier(sessionId, 'sessionId');
|
|
134
|
+
await state.scopeQueue;
|
|
135
|
+
this.core.imports.assertNoCommittingSessionImport(state, sessionId);
|
|
136
|
+
return this.startSessionDeletion(state, scopeId, scope.scope, sessionId);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private startSessionDeletion(
|
|
140
|
+
state: IStorageState,
|
|
141
|
+
scopeId: string,
|
|
142
|
+
scope: TScope,
|
|
143
|
+
sessionId: string,
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
const deletionKey = state.tombstones.get(sessionId)?.rootSessionId ?? sessionId;
|
|
146
|
+
return this.registerSessionDeletion(
|
|
147
|
+
state,
|
|
148
|
+
deletionKey,
|
|
149
|
+
() => this.deleteSessionInternal(state, scopeId, scope, sessionId),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private startReservedSessionDeletion(
|
|
154
|
+
state: IStorageState,
|
|
155
|
+
scopeId: string,
|
|
156
|
+
scope: TScope,
|
|
157
|
+
sessionId: string,
|
|
158
|
+
reservation: IReservedSessionDeletion,
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
return this.registerSessionDeletion(
|
|
161
|
+
state,
|
|
162
|
+
sessionId,
|
|
163
|
+
() => this.finishReservedSessionDeletion(
|
|
164
|
+
state,
|
|
165
|
+
scopeId,
|
|
166
|
+
scope,
|
|
167
|
+
sessionId,
|
|
168
|
+
reservation,
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private registerSessionDeletion(
|
|
174
|
+
state: IStorageState,
|
|
175
|
+
deletionKey: string,
|
|
176
|
+
operation: () => Promise<void>,
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
const existingDeletion = state.sessionDeletions.get(deletionKey);
|
|
179
|
+
if (existingDeletion) return existingDeletion;
|
|
180
|
+
let deletion!: Promise<void>;
|
|
181
|
+
deletion = operation().finally(() => {
|
|
182
|
+
if (state.sessionDeletions.get(deletionKey) === deletion) {
|
|
183
|
+
state.sessionDeletions.delete(deletionKey);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
state.sessionDeletions.set(deletionKey, deletion);
|
|
187
|
+
return deletion;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private assertAuthorizedSessionGenerationCohort(
|
|
191
|
+
entries: readonly (IFlexSession | IFlexSessionTombstone)[],
|
|
192
|
+
authorizedBySessionId: ReadonlyMap<
|
|
193
|
+
string,
|
|
194
|
+
Readonly<IFlexSessionGenerationCohortEntry>
|
|
195
|
+
>,
|
|
196
|
+
): void {
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
const authorized = authorizedBySessionId.get(entry.sessionId);
|
|
199
|
+
if (!authorized || !matchesSessionGeneration(entry, authorized)) {
|
|
200
|
+
throw new FlexHarnessValidationError(
|
|
201
|
+
`authorizedCohort does not authorize the exact generation of session "${entry.sessionId}".`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private reserveLiveSessionDeletion(
|
|
208
|
+
state: IStorageState,
|
|
209
|
+
sessionId: string,
|
|
210
|
+
stored: IStoredSessionState,
|
|
211
|
+
subtree: readonly IStoredSessionState[],
|
|
212
|
+
descendantRoots: readonly string[],
|
|
213
|
+
): IReservedSessionDeletion {
|
|
214
|
+
if (state.sessions.get(sessionId) !== stored) {
|
|
215
|
+
throw new FlexHarnessNotFoundError('Session', sessionId);
|
|
216
|
+
}
|
|
217
|
+
const rootDepth = stored.session.depth ?? 0;
|
|
218
|
+
const deletedAt = new Date().toISOString();
|
|
219
|
+
const deletedSessions = subtree.map((entry) => cloneSerializable(entry.session));
|
|
220
|
+
const provenanceBySessionId = new Map(subtree.map((entry) => [
|
|
221
|
+
entry.session.sessionId,
|
|
222
|
+
this.core.subagentTasks.createSubagentProvenance(state, entry.session),
|
|
223
|
+
]));
|
|
224
|
+
for (const entry of subtree) {
|
|
225
|
+
const entrySessionId = entry.session.sessionId;
|
|
226
|
+
const subagent = provenanceBySessionId.get(entrySessionId);
|
|
227
|
+
state.sessions.delete(entrySessionId);
|
|
228
|
+
state.retainedSessionCleanups.set(entrySessionId, {
|
|
229
|
+
stored: entry,
|
|
230
|
+
domainsCompleted: false,
|
|
231
|
+
});
|
|
232
|
+
state.tombstones.set(entrySessionId, {
|
|
233
|
+
sessionId: entrySessionId,
|
|
234
|
+
...requireSessionGeneration(entry.session),
|
|
235
|
+
deletedAt,
|
|
236
|
+
rootSessionId: sessionId,
|
|
237
|
+
depth: (entry.session.depth ?? rootDepth) - rootDepth,
|
|
238
|
+
...(entry.session.parentSessionId === undefined
|
|
239
|
+
? {}
|
|
240
|
+
: { parentSessionId: entry.session.parentSessionId }),
|
|
241
|
+
...(subagent === undefined ? {} : { subagent: cloneSerializable(subagent) }),
|
|
242
|
+
...(entry.session.origin === undefined ? {} : { imported: true as const }),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
deletedSessions,
|
|
247
|
+
descendantRoots: [...descendantRoots],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
public async deleteSessionInternal(
|
|
252
|
+
state: IStorageState,
|
|
253
|
+
scopeId: string,
|
|
254
|
+
scope: TScope,
|
|
255
|
+
sessionId: string,
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
const existingTombstone = state.tombstones.get(sessionId);
|
|
258
|
+
if (existingTombstone) {
|
|
259
|
+
const rootSessionId = existingTombstone.rootSessionId ?? existingTombstone.sessionId;
|
|
260
|
+
const descendantRoots = this.descendantTombstoneRoots(state, sessionId)
|
|
261
|
+
.filter((candidate) => candidate !== rootSessionId);
|
|
262
|
+
try {
|
|
263
|
+
const orphanErrors = await this.core.lifecycle.closeOrphanedResources(state.storageKey);
|
|
264
|
+
if (orphanErrors.length > 0) throw combineErrors(orphanErrors);
|
|
265
|
+
for (const descendantRoot of descendantRoots) {
|
|
266
|
+
await this.finishTombstoneCleanup(
|
|
267
|
+
state,
|
|
268
|
+
descendantRoot,
|
|
269
|
+
scopeId,
|
|
270
|
+
{ scopeId, scope },
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
await this.finishTombstoneCleanup(state, rootSessionId, scopeId, { scopeId, scope });
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw this.core.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-delete');
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
280
|
+
const reservation = await this.core.mutateScope(state, () => {
|
|
281
|
+
const subtree = this.collectSessionSubtree(state, sessionId);
|
|
282
|
+
const descendantRoots = this.descendantTombstoneRoots(state, sessionId);
|
|
283
|
+
return this.reserveLiveSessionDeletion(
|
|
284
|
+
state,
|
|
285
|
+
sessionId,
|
|
286
|
+
stored,
|
|
287
|
+
subtree,
|
|
288
|
+
descendantRoots,
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
await this.finishReservedSessionDeletion(state, scopeId, scope, sessionId, reservation);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private async finishReservedSessionDeletion(
|
|
295
|
+
state: IStorageState,
|
|
296
|
+
scopeId: string,
|
|
297
|
+
scope: TScope,
|
|
298
|
+
sessionId: string,
|
|
299
|
+
reservation: IReservedSessionDeletion,
|
|
300
|
+
): Promise<void> {
|
|
301
|
+
const { deletedSessions, descendantRoots } = reservation;
|
|
302
|
+
const reason = this.core.trustInternalError(new FlexHarnessAbortError('The session subtree was deleted.'));
|
|
303
|
+
const configurationSettlement = this.core.configurations.abortSessionConfigurations(
|
|
304
|
+
state.storageKey,
|
|
305
|
+
reason,
|
|
306
|
+
new Set(deletedSessions.map((deleted) => deleted.sessionId)),
|
|
307
|
+
);
|
|
308
|
+
if (configurationSettlement) await configurationSettlement;
|
|
309
|
+
const slashCommandErrors: unknown[] = [];
|
|
310
|
+
for (const deleted of deletedSessions) {
|
|
311
|
+
const listingSettlement = this.core.commands.abortSlashCommandListings(
|
|
312
|
+
state.storageKey,
|
|
313
|
+
reason,
|
|
314
|
+
deleted.sessionId,
|
|
315
|
+
);
|
|
316
|
+
if (listingSettlement) await listingSettlement;
|
|
317
|
+
await this.core.commands.abortSlashCommandExecutions(
|
|
318
|
+
state.storageKey,
|
|
319
|
+
reason,
|
|
320
|
+
slashCommandErrors,
|
|
321
|
+
deleted.sessionId,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
const childFirstDeletedSessions = [...deletedSessions].sort((left, right) =>
|
|
325
|
+
(right.depth ?? 0) - (left.depth ?? 0)
|
|
326
|
+
|| left.sessionId.localeCompare(right.sessionId));
|
|
327
|
+
for (const deleted of childFirstDeletedSessions) {
|
|
328
|
+
const active = state.activeRuns.get(deleted.sessionId);
|
|
329
|
+
if (active) {
|
|
330
|
+
this.core.runs.cancelRun(
|
|
331
|
+
active,
|
|
332
|
+
reason,
|
|
333
|
+
this.core.compaction.createCompactorContext(scopeId, scope, state.storageKey, deleted.sessionId),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const orphanErrors = [
|
|
339
|
+
...slashCommandErrors,
|
|
340
|
+
...await this.core.lifecycle.closeOrphanedResources(state.storageKey),
|
|
341
|
+
];
|
|
342
|
+
if (orphanErrors.length > 0) throw combineErrors(orphanErrors);
|
|
343
|
+
for (const descendantRoot of descendantRoots) {
|
|
344
|
+
await this.finishTombstoneCleanup(
|
|
345
|
+
state,
|
|
346
|
+
descendantRoot,
|
|
347
|
+
scopeId,
|
|
348
|
+
{ scopeId, scope },
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
await this.finishTombstoneCleanup(state, sessionId, scopeId, { scopeId, scope });
|
|
352
|
+
} catch (error) {
|
|
353
|
+
throw this.core.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-delete');
|
|
354
|
+
}
|
|
355
|
+
for (const deleted of childFirstDeletedSessions) {
|
|
356
|
+
this.core.emitEvent(scopeId, deleted, {
|
|
357
|
+
type: 'session.deleted',
|
|
358
|
+
session: publicSnapshot(deleted),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
public async cleanupSessionDomains(
|
|
364
|
+
state: IStorageState,
|
|
365
|
+
tombstone: IFlexSessionTombstone,
|
|
366
|
+
): Promise<void> {
|
|
367
|
+
const storageKey = state.storageKey;
|
|
368
|
+
const sessionId = tombstone.sessionId;
|
|
369
|
+
const projectManagementCleanup = async (): Promise<void> => {
|
|
370
|
+
await this.core.projects.projectManagementQueues.get(
|
|
371
|
+
this.core.projects.projectManagementKey(storageKey, sessionId),
|
|
372
|
+
);
|
|
373
|
+
await this.core.projects.tombstoneProjectManagementSession(
|
|
374
|
+
state,
|
|
375
|
+
tombstone,
|
|
376
|
+
);
|
|
377
|
+
};
|
|
378
|
+
const resultStore = this.core.stores.results;
|
|
379
|
+
const resultCleanup = async (): Promise<void> => {
|
|
380
|
+
// A result being saved for the session is written before its results are deleted.
|
|
381
|
+
await this.core.results.resultQueues.get(this.core.results.resultQueueKey(storageKey, sessionId));
|
|
382
|
+
await resultStore!.deleteSession(storageKey, sessionId);
|
|
383
|
+
};
|
|
384
|
+
const results = await Promise.allSettled([
|
|
385
|
+
this.core.stores.agentEvents.deleteSession(storageKey, sessionId),
|
|
386
|
+
this.core.stores.jobs.deleteSession(storageKey, sessionId),
|
|
387
|
+
this.core.stores.projections.deleteSession(storageKey, sessionId),
|
|
388
|
+
this.core.stores.permissions.deleteSession(storageKey, sessionId),
|
|
389
|
+
...(resultStore ? [resultCleanup()] : []),
|
|
390
|
+
// An imported session refuses every project-management write, so it has no state to fence.
|
|
391
|
+
...(tombstone.imported ? [] : [projectManagementCleanup()]),
|
|
392
|
+
]);
|
|
393
|
+
const errors = results
|
|
394
|
+
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
395
|
+
.map((result) => result.reason);
|
|
396
|
+
if (errors.length > 0) throw combineErrors(errors);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private collectSessionSubtree(
|
|
400
|
+
state: IStorageState,
|
|
401
|
+
rootSessionId: string,
|
|
402
|
+
): IStoredSessionState[] {
|
|
403
|
+
const subtree: IStoredSessionState[] = [];
|
|
404
|
+
const pending = [rootSessionId];
|
|
405
|
+
const seen = new Set<string>();
|
|
406
|
+
while (pending.length > 0) {
|
|
407
|
+
const sessionId = pending.pop()!;
|
|
408
|
+
if (seen.has(sessionId)) {
|
|
409
|
+
throw new FlexHarnessValidationError('Session relationships contain a cycle.');
|
|
410
|
+
}
|
|
411
|
+
seen.add(sessionId);
|
|
412
|
+
const stored = state.sessions.get(sessionId);
|
|
413
|
+
if (!stored) continue;
|
|
414
|
+
subtree.push(stored);
|
|
415
|
+
const children = [...state.sessions.values()]
|
|
416
|
+
.filter((candidate) => candidate.session.parentSessionId === sessionId)
|
|
417
|
+
.map((candidate) => candidate.session.sessionId)
|
|
418
|
+
.sort()
|
|
419
|
+
.reverse();
|
|
420
|
+
pending.push(...children);
|
|
421
|
+
}
|
|
422
|
+
return subtree;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
public tombstoneGroup(
|
|
426
|
+
state: IStorageState,
|
|
427
|
+
rootSessionId: string,
|
|
428
|
+
): IFlexSessionTombstone[] {
|
|
429
|
+
return [...state.tombstones.values()]
|
|
430
|
+
.filter((tombstone) =>
|
|
431
|
+
(tombstone.rootSessionId ?? tombstone.sessionId) === rootSessionId)
|
|
432
|
+
.sort((left, right) =>
|
|
433
|
+
(right.depth ?? 0) - (left.depth ?? 0)
|
|
434
|
+
|| left.sessionId.localeCompare(right.sessionId));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private tombstonesForRoots(
|
|
438
|
+
state: IStorageState,
|
|
439
|
+
rootSessionIds: readonly string[],
|
|
440
|
+
): IFlexSessionTombstone[] {
|
|
441
|
+
return [...new Set(rootSessionIds)]
|
|
442
|
+
.flatMap((rootSessionId) => this.tombstoneGroup(state, rootSessionId));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
public descendantTombstoneRoots(
|
|
446
|
+
state: IStorageState,
|
|
447
|
+
ancestorSessionId: string,
|
|
448
|
+
): string[] {
|
|
449
|
+
return [...new Set([...state.tombstones.values()]
|
|
450
|
+
.map((tombstone) => tombstone.rootSessionId ?? tombstone.sessionId)
|
|
451
|
+
.filter((rootSessionId) =>
|
|
452
|
+
rootSessionId !== ancestorSessionId
|
|
453
|
+
&& this.core.isSessionAncestor(state, ancestorSessionId, rootSessionId)))]
|
|
454
|
+
.sort((left, right) =>
|
|
455
|
+
this.core.sessionAncestryDepth(state, right) - this.core.sessionAncestryDepth(state, left)
|
|
456
|
+
|| left.localeCompare(right));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
public orderTombstoneRootsChildFirst(
|
|
460
|
+
state: IStorageState,
|
|
461
|
+
rootSessionIds: readonly string[],
|
|
462
|
+
): string[] {
|
|
463
|
+
return [...rootSessionIds].sort((left, right) =>
|
|
464
|
+
this.core.sessionAncestryDepth(state, right) - this.core.sessionAncestryDepth(state, left)
|
|
465
|
+
|| left.localeCompare(right));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
public finishTombstoneCleanup(
|
|
469
|
+
state: IStorageState,
|
|
470
|
+
rootSessionId: string,
|
|
471
|
+
scopeId: string,
|
|
472
|
+
invocation?: { scopeId: string; scope: TScope },
|
|
473
|
+
): Promise<void> {
|
|
474
|
+
const existing = state.tombstoneCleanups.get(rootSessionId);
|
|
475
|
+
if (existing) return existing;
|
|
476
|
+
let cleanup!: Promise<void>;
|
|
477
|
+
cleanup = this.finishTombstoneCleanupInternal(
|
|
478
|
+
state,
|
|
479
|
+
rootSessionId,
|
|
480
|
+
scopeId,
|
|
481
|
+
invocation,
|
|
482
|
+
).finally(() => {
|
|
483
|
+
if (state.tombstoneCleanups.get(rootSessionId) === cleanup) {
|
|
484
|
+
state.tombstoneCleanups.delete(rootSessionId);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
state.tombstoneCleanups.set(rootSessionId, cleanup);
|
|
488
|
+
return cleanup;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private async finishTombstoneCleanupInternal(
|
|
492
|
+
state: IStorageState,
|
|
493
|
+
rootSessionId: string,
|
|
494
|
+
scopeId: string,
|
|
495
|
+
invocation?: { scopeId: string; scope: TScope },
|
|
496
|
+
): Promise<void> {
|
|
497
|
+
for (const descendantRoot of this.descendantTombstoneRoots(state, rootSessionId)) {
|
|
498
|
+
await this.finishTombstoneCleanup(state, descendantRoot, scopeId, invocation);
|
|
499
|
+
}
|
|
500
|
+
const group = this.tombstoneGroup(state, rootSessionId);
|
|
501
|
+
if (group.length === 0) return;
|
|
502
|
+
const exactInvocation = invocation ?? {
|
|
503
|
+
scopeId: state.scopeContext.scopeId,
|
|
504
|
+
scope: state.scopeContext.scope as TScope,
|
|
505
|
+
};
|
|
506
|
+
const groupIds = new Set(group.map((tombstone) => tombstone.sessionId));
|
|
507
|
+
const reason = this.core.trustInternalError(new FlexHarnessAbortError('The session subtree was deleted.'));
|
|
508
|
+
const contextFor = (
|
|
509
|
+
sessionId: string,
|
|
510
|
+
retained?: IRetainedSessionCleanup,
|
|
511
|
+
): IFlexAgentContextInvocation<TScope> | undefined => exactInvocation
|
|
512
|
+
? this.core.compaction.createCompactorContext(
|
|
513
|
+
exactInvocation.scopeId,
|
|
514
|
+
exactInvocation.scope,
|
|
515
|
+
state.storageKey,
|
|
516
|
+
sessionId,
|
|
517
|
+
)
|
|
518
|
+
: retained?.stored.compactorContext as IFlexAgentContextInvocation<TScope> | undefined;
|
|
519
|
+
for (const sessionId of groupIds) {
|
|
520
|
+
const retained = state.retainedSessionCleanups.get(sessionId);
|
|
521
|
+
const context = contextFor(sessionId, retained);
|
|
522
|
+
if (retained) {
|
|
523
|
+
this.core.compaction.abortCompactorLifecycle(retained.stored, reason);
|
|
524
|
+
this.core.prompts.cancelStoredPromptQueue(retained.stored, reason, undefined, context);
|
|
525
|
+
}
|
|
526
|
+
const run = state.activeRuns.get(sessionId);
|
|
527
|
+
if (run) this.core.runs.cancelRun(run, reason, context);
|
|
528
|
+
}
|
|
529
|
+
await Promise.allSettled([...groupIds]
|
|
530
|
+
.map((sessionId) => state.sessionInitializations.get(sessionId))
|
|
531
|
+
.filter((completion): completion is Promise<void> => completion !== undefined));
|
|
532
|
+
for (const tombstone of group) {
|
|
533
|
+
const sessionId = tombstone.sessionId;
|
|
534
|
+
const retained = state.retainedSessionCleanups.get(sessionId);
|
|
535
|
+
const errors: unknown[] = [];
|
|
536
|
+
const run = state.activeRuns.get(sessionId);
|
|
537
|
+
const context = contextFor(sessionId, retained);
|
|
538
|
+
if (retained && !retained.stored.agentSessionAbortCompleted) {
|
|
539
|
+
try {
|
|
540
|
+
await this.core.lifecycle.abortStoredSession(retained.stored, reason, context);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
errors.push(this.core.projectOperationError(
|
|
543
|
+
error,
|
|
544
|
+
'agentSession',
|
|
545
|
+
scopeId,
|
|
546
|
+
sessionId,
|
|
547
|
+
'session-delete',
|
|
548
|
+
));
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (run) {
|
|
552
|
+
const settled = await Promise.allSettled([run.completion]);
|
|
553
|
+
if (settled[0].status === 'rejected') {
|
|
554
|
+
this.core.appendUnexpectedErrors(errors, settled[0].reason);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
errors.push(...await this.core.admission.settleDelegatedRunAdmissionOwners(
|
|
558
|
+
this.core.admission.delegatedRunAdmissionOwnersForGeneration(state, tombstone),
|
|
559
|
+
));
|
|
560
|
+
if (retained) {
|
|
561
|
+
if (retained.stored.promptQueueDrain) {
|
|
562
|
+
const settled = await Promise.allSettled([retained.stored.promptQueueDrain]);
|
|
563
|
+
if (settled[0].status === 'rejected') errors.push(settled[0].reason);
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
await this.core.lifecycle.closeStoredSession(retained.stored, context);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
this.core.appendUnexpectedErrors(errors, this.core.projectOperationError(
|
|
569
|
+
error,
|
|
570
|
+
'toolCleanup',
|
|
571
|
+
scopeId,
|
|
572
|
+
sessionId,
|
|
573
|
+
'session-delete',
|
|
574
|
+
));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (errors.length > 0) {
|
|
578
|
+
if (
|
|
579
|
+
retained
|
|
580
|
+
&& state.lifecycle === 'retired'
|
|
581
|
+
&& !this.core.lifecycle.storedSessionCleanupCompleted(retained.stored)
|
|
582
|
+
) this.core.lifecycle.orphanedStoredSessions.add(retained.stored);
|
|
583
|
+
throw combineErrors(errors);
|
|
584
|
+
}
|
|
585
|
+
if (retained) this.core.prompts.purgeStoredPromptQueue(retained.stored);
|
|
586
|
+
if (!retained?.domainsCompleted) {
|
|
587
|
+
if (this.core.lifecycle.hasOrphanedSessionResources(state.storageKey, sessionId)) {
|
|
588
|
+
throw new Error(`Session "${sessionId}" still has runtime cleanup ownership.`);
|
|
589
|
+
}
|
|
590
|
+
const releaseContext = exactInvocation ?? retained?.stored.compactorContext as
|
|
591
|
+
IFlexAgentContextInvocation<TScope> | undefined;
|
|
592
|
+
if (!releaseContext) {
|
|
593
|
+
throw new FlexHarnessValidationError(
|
|
594
|
+
'Tombstone cleanup requires its exact scope context before deleting projection data.',
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
await this.core.reversions.releaseDeletedSessionReversions(
|
|
598
|
+
state,
|
|
599
|
+
state.storageKey,
|
|
600
|
+
tombstone,
|
|
601
|
+
releaseContext.scopeId,
|
|
602
|
+
releaseContext.scope,
|
|
603
|
+
retained?.stored,
|
|
604
|
+
);
|
|
605
|
+
await this.cleanupSessionDomains(state, tombstone);
|
|
606
|
+
if (retained) retained.domainsCompleted = true;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
await this.core.mutateScope(state, () => {
|
|
610
|
+
for (const tombstone of group) {
|
|
611
|
+
state.tombstones.delete(tombstone.sessionId);
|
|
612
|
+
state.retainedSessionCleanups.delete(tombstone.sessionId);
|
|
613
|
+
}
|
|
614
|
+
}, true);
|
|
615
|
+
}
|
|
616
|
+
}
|