@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,1684 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
FlexHarnessAbortError,
|
|
4
|
+
FlexHarnessSessionBusyError,
|
|
5
|
+
FlexHarnessSlashCommandReentryError,
|
|
6
|
+
FlexHarnessSlashCommandUnavailableError,
|
|
7
|
+
FlexHarnessValidationError,
|
|
8
|
+
} from './errors.js';
|
|
9
|
+
import {
|
|
10
|
+
FLEX_REVERSION_MAX_AFFECTED_WORKSPACES,
|
|
11
|
+
FLEX_REVERSION_REASON_CODE_MAX_BYTES,
|
|
12
|
+
FLEX_REVERSION_REFERENCE_MAX_BYTES,
|
|
13
|
+
FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES,
|
|
14
|
+
FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES,
|
|
15
|
+
type IFlexAgentContextInvocation,
|
|
16
|
+
type IFlexAffectedWorkspace,
|
|
17
|
+
type IFlexProjectionSnapshotCurrent,
|
|
18
|
+
type IFlexReversionSegment,
|
|
19
|
+
type IFlexPendingReversionRelease,
|
|
20
|
+
type IFlexRedoSessionResult,
|
|
21
|
+
type IFlexSessionGeneration,
|
|
22
|
+
type IFlexSessionReversionGroup,
|
|
23
|
+
type IFlexSessionReversionInfo,
|
|
24
|
+
type IFlexSessionTombstone,
|
|
25
|
+
type IFlexUndoSessionResult,
|
|
26
|
+
type TFlexProjectionSnapshot,
|
|
27
|
+
type TFlexTurnReversionFinalizedOutcome,
|
|
28
|
+
type TJsonValue,
|
|
29
|
+
} from './interfaces.js';
|
|
30
|
+
import { cloneSerializable, normalizeJsonValue } from './utils.json.js';
|
|
31
|
+
import { publicSnapshot } from './utils.transfer.js';
|
|
32
|
+
import type {
|
|
33
|
+
TCanonicalOutcome,
|
|
34
|
+
IStoredSessionState,
|
|
35
|
+
IReversionGroup,
|
|
36
|
+
TNormalizedCaptureInspection,
|
|
37
|
+
IStorageState,
|
|
38
|
+
IActiveRun,
|
|
39
|
+
} from './interfaces.flexharness.js';
|
|
40
|
+
import {
|
|
41
|
+
slashCommandSessionUnavailableReason,
|
|
42
|
+
requireSessionGeneration,
|
|
43
|
+
sha256Hex,
|
|
44
|
+
combineErrors,
|
|
45
|
+
} from './utils.flexharness.js';
|
|
46
|
+
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
47
|
+
|
|
48
|
+
/** Turn reversion: capture, undo and redo, and the release of reversion state. */
|
|
49
|
+
export class FlexHarnessReversions<TScope> {
|
|
50
|
+
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
51
|
+
|
|
52
|
+
public async getSessionReversionInfo(
|
|
53
|
+
scopeId: string,
|
|
54
|
+
sessionId: string,
|
|
55
|
+
): Promise<IFlexSessionReversionInfo> {
|
|
56
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
57
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
58
|
+
await stored.projectionQueue;
|
|
59
|
+
this.core.assertStateAcceptingWork(state);
|
|
60
|
+
await this.reconcileContextAvailability(state, stored);
|
|
61
|
+
const unavailableReason = await this.reversionUnavailableReason(state, stored);
|
|
62
|
+
const groups = this.reversionGroups(stored);
|
|
63
|
+
const undo = this.reversionUnit(stored, 'undo');
|
|
64
|
+
const redo = this.reversionUnit(stored, 'redo');
|
|
65
|
+
const isAvailable = (unit: ReturnType<typeof this.reversionUnit>) => Boolean(
|
|
66
|
+
!unavailableReason
|
|
67
|
+
&& unit
|
|
68
|
+
&& unit.target.contextAvailable
|
|
69
|
+
&& !unit.segments.some((segment) =>
|
|
70
|
+
segment.provenance === 'workspace'
|
|
71
|
+
&& segment.disposition === 'revertible'
|
|
72
|
+
&& segment.workspaceReference === undefined),
|
|
73
|
+
);
|
|
74
|
+
return publicSnapshot({
|
|
75
|
+
undoAvailable: isAvailable(undo),
|
|
76
|
+
redoAvailable: isAvailable(redo),
|
|
77
|
+
groups: groups.map((group, index): IFlexSessionReversionGroup => ({
|
|
78
|
+
runId: group.target.runId,
|
|
79
|
+
kind: group.kind,
|
|
80
|
+
visibility: index < stored.revertCursor ? 'visible' : 'hidden',
|
|
81
|
+
affectedWorkspaces: cloneSerializable(group.affectedWorkspaces),
|
|
82
|
+
affectedWorkspacesTruncated: group.affectedWorkspacesTruncated,
|
|
83
|
+
})),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public async undoSession(
|
|
88
|
+
scopeId: string,
|
|
89
|
+
sessionId: string,
|
|
90
|
+
signal?: AbortSignal,
|
|
91
|
+
): Promise<IFlexUndoSessionResult> {
|
|
92
|
+
const result = await this.changeReversionCursor(scopeId, sessionId, 'undo', signal);
|
|
93
|
+
return Object.freeze({ revertedRunId: result });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public async redoSession(
|
|
97
|
+
scopeId: string,
|
|
98
|
+
sessionId: string,
|
|
99
|
+
signal?: AbortSignal,
|
|
100
|
+
): Promise<IFlexRedoSessionResult> {
|
|
101
|
+
const result = await this.changeReversionCursor(scopeId, sessionId, 'redo', signal);
|
|
102
|
+
return Object.freeze({ restoredRunId: result });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public reversionUnit(
|
|
106
|
+
stored: IStoredSessionState,
|
|
107
|
+
direction: 'undo' | 'redo',
|
|
108
|
+
): { target: IFlexReversionSegment; segments: IFlexReversionSegment[]; toCursor: number } | undefined {
|
|
109
|
+
const groups = this.reversionGroups(stored);
|
|
110
|
+
if (direction === 'undo') {
|
|
111
|
+
let start = stored.revertCursor - 1;
|
|
112
|
+
while (start >= 0 && groups[start]?.kind === 'no-change') start--;
|
|
113
|
+
const candidate = groups[start];
|
|
114
|
+
if (!candidate || candidate.kind !== 'candidate') return undefined;
|
|
115
|
+
return {
|
|
116
|
+
target: candidate.target,
|
|
117
|
+
segments: groups.slice(start, stored.revertCursor).flatMap((group) => group.segments),
|
|
118
|
+
toCursor: start,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const candidate = groups[stored.revertCursor];
|
|
122
|
+
if (!candidate || candidate.kind !== 'candidate') return undefined;
|
|
123
|
+
let end = stored.revertCursor + 1;
|
|
124
|
+
while (groups[end]?.kind === 'no-change') end++;
|
|
125
|
+
return {
|
|
126
|
+
target: candidate.target,
|
|
127
|
+
segments: groups.slice(stored.revertCursor, end).flatMap((group) => group.segments),
|
|
128
|
+
toCursor: end,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public reversionGroups(stored: IStoredSessionState): IReversionGroup[] {
|
|
133
|
+
const groups: IReversionGroup[] = [];
|
|
134
|
+
let leading: IFlexReversionSegment[] = [];
|
|
135
|
+
for (const segment of stored.reversionSegments) {
|
|
136
|
+
if (segment.status === 'completed') {
|
|
137
|
+
const segments = groups.length === 0 ? [...leading, segment] : [segment];
|
|
138
|
+
leading = [];
|
|
139
|
+
groups.push({
|
|
140
|
+
target: segment,
|
|
141
|
+
segments,
|
|
142
|
+
kind: 'candidate',
|
|
143
|
+
affectedWorkspaces: [],
|
|
144
|
+
affectedWorkspacesTruncated: false,
|
|
145
|
+
});
|
|
146
|
+
} else if (groups.length > 0) {
|
|
147
|
+
groups[groups.length - 1].segments.push(segment);
|
|
148
|
+
} else {
|
|
149
|
+
leading.push(segment);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const group of groups) {
|
|
153
|
+
const affected = new Map<string, IFlexAffectedWorkspace>();
|
|
154
|
+
for (const workspace of group.segments.flatMap((segment) =>
|
|
155
|
+
segment.affectedWorkspaces ?? [])) {
|
|
156
|
+
if (affected.has(workspace.id)) continue;
|
|
157
|
+
if (affected.size < FLEX_REVERSION_MAX_AFFECTED_WORKSPACES) {
|
|
158
|
+
affected.set(workspace.id, cloneSerializable(workspace));
|
|
159
|
+
} else {
|
|
160
|
+
group.affectedWorkspacesTruncated = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
group.affectedWorkspaces = [...affected.values()];
|
|
164
|
+
if (this.core.reversionPolicy === 'transcript-optional') continue;
|
|
165
|
+
if (group.segments.some((segment) =>
|
|
166
|
+
segment.status === 'capturing'
|
|
167
|
+
|| segment.disposition === 'pending'
|
|
168
|
+
|| (segment.protocolVersion === 1 && segment.provenance === 'transcript')
|
|
169
|
+
|| segment.disposition === 'nonrevertible')) {
|
|
170
|
+
group.kind = 'barrier';
|
|
171
|
+
} else if (group.segments.some((segment) => segment.disposition === 'revertible')) {
|
|
172
|
+
group.kind = 'candidate';
|
|
173
|
+
} else {
|
|
174
|
+
group.kind = 'no-change';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return groups;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private async requireReversionIdle(
|
|
181
|
+
state: IStorageState,
|
|
182
|
+
stored: IStoredSessionState,
|
|
183
|
+
sessionId: string,
|
|
184
|
+
allowPendingApply = false,
|
|
185
|
+
operationSignal?: AbortSignal,
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
const reason = await this.reversionUnavailableReason(
|
|
188
|
+
state,
|
|
189
|
+
stored,
|
|
190
|
+
allowPendingApply,
|
|
191
|
+
operationSignal,
|
|
192
|
+
);
|
|
193
|
+
if (reason) throw new FlexHarnessSessionBusyError(sessionId, reason.toLowerCase().replace(/\.$/u, ''));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public async reversionUnavailableReason(
|
|
197
|
+
state: IStorageState,
|
|
198
|
+
stored: IStoredSessionState,
|
|
199
|
+
allowPendingApply = false,
|
|
200
|
+
operationSignal?: AbortSignal,
|
|
201
|
+
): Promise<string | undefined> {
|
|
202
|
+
if (stored.pendingReversion && !(allowPendingApply && stored.pendingReversion.kind === 'apply')) {
|
|
203
|
+
return 'Session has a pending reversion operation.';
|
|
204
|
+
}
|
|
205
|
+
if (!this.core.prompts.isSessionRuntimeIdle(state, stored)) {
|
|
206
|
+
return slashCommandSessionUnavailableReason;
|
|
207
|
+
}
|
|
208
|
+
const activeOperation = this.core.commands.activeSlashCommandExecutions.get(
|
|
209
|
+
this.core.commands.slashCommandSessionKey(state.storageKey, stored.session.sessionId),
|
|
210
|
+
);
|
|
211
|
+
if (activeOperation && activeOperation.controller.signal !== operationSignal) {
|
|
212
|
+
return 'Session already has an active slash command execution.';
|
|
213
|
+
}
|
|
214
|
+
if ([...state.pendingPermissions.values()].some((pending) =>
|
|
215
|
+
!pending.settled && pending.request.sessionId === stored.session.sessionId)) {
|
|
216
|
+
return 'Session has a pending permission.';
|
|
217
|
+
}
|
|
218
|
+
if (stored.agentSession.listUncertainToolExecutions().length > 0) {
|
|
219
|
+
return 'Session has uncertain tool executions.';
|
|
220
|
+
}
|
|
221
|
+
if (stored.jobs) {
|
|
222
|
+
operationSignal?.throwIfAborted();
|
|
223
|
+
const jobs = Promise.resolve().then(() => stored.jobs!.list());
|
|
224
|
+
if ((await this.awaitReversionOperation(jobs, operationSignal))
|
|
225
|
+
.some((job) => job.state === 'running')) {
|
|
226
|
+
return 'Session has running background jobs.';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public async awaitReversionOperation<TResult>(
|
|
233
|
+
operation: Promise<TResult>,
|
|
234
|
+
signal?: AbortSignal,
|
|
235
|
+
): Promise<TResult> {
|
|
236
|
+
if (!signal) return operation;
|
|
237
|
+
void operation.catch(() => undefined);
|
|
238
|
+
let rejectForAbort!: () => void;
|
|
239
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
240
|
+
rejectForAbort = () => reject(
|
|
241
|
+
signal.reason ?? this.core.trustInternalError(new FlexHarnessAbortError('The slash command was aborted.')),
|
|
242
|
+
);
|
|
243
|
+
signal.addEventListener('abort', rejectForAbort, { once: true });
|
|
244
|
+
if (signal.aborted) rejectForAbort();
|
|
245
|
+
});
|
|
246
|
+
try {
|
|
247
|
+
return await Promise.race([operation, aborted]);
|
|
248
|
+
} finally {
|
|
249
|
+
signal.removeEventListener('abort', rejectForAbort);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private async changeReversionCursor(
|
|
254
|
+
scopeId: string,
|
|
255
|
+
sessionId: string,
|
|
256
|
+
direction: 'undo' | 'redo',
|
|
257
|
+
signal?: AbortSignal,
|
|
258
|
+
): Promise<string> {
|
|
259
|
+
const { scope, state } = await this.core.resolveState(scopeId);
|
|
260
|
+
return this.changeReversionCursorResolved(
|
|
261
|
+
scopeId,
|
|
262
|
+
sessionId,
|
|
263
|
+
direction,
|
|
264
|
+
scope.scope,
|
|
265
|
+
state,
|
|
266
|
+
signal,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public async changeReversionCursorResolved(
|
|
271
|
+
scopeId: string,
|
|
272
|
+
sessionId: string,
|
|
273
|
+
direction: 'undo' | 'redo',
|
|
274
|
+
scope: TScope,
|
|
275
|
+
state: IStorageState,
|
|
276
|
+
signal?: AbortSignal,
|
|
277
|
+
): Promise<string> {
|
|
278
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
279
|
+
this.core.assertSessionWritable(stored);
|
|
280
|
+
this.core.assertStateAcceptingWork(state);
|
|
281
|
+
const invocationOwner = this.core.commands.slashCommandInvocationContext.getStore();
|
|
282
|
+
if (invocationOwner?.sessionKey === this.core.commands.slashCommandSessionKey(state.storageKey, sessionId)) {
|
|
283
|
+
throw this.core.trustInternalError(new FlexHarnessSlashCommandReentryError(sessionId));
|
|
284
|
+
}
|
|
285
|
+
return this.core.commands.trackSlashCommandExecution(
|
|
286
|
+
scopeId,
|
|
287
|
+
state,
|
|
288
|
+
sessionId,
|
|
289
|
+
'operation',
|
|
290
|
+
signal,
|
|
291
|
+
async (operationSignal) => {
|
|
292
|
+
await stored.projectionQueue;
|
|
293
|
+
const resumable = stored.pendingReversion?.kind === 'apply'
|
|
294
|
+
&& stored.pendingReversion.direction === direction;
|
|
295
|
+
await this.requireReversionIdle(
|
|
296
|
+
state,
|
|
297
|
+
stored,
|
|
298
|
+
sessionId,
|
|
299
|
+
resumable,
|
|
300
|
+
operationSignal,
|
|
301
|
+
);
|
|
302
|
+
await this.reconcileContextAvailability(state, stored);
|
|
303
|
+
operationSignal.throwIfAborted();
|
|
304
|
+
const pending = stored.pendingReversion;
|
|
305
|
+
if (pending?.kind === 'apply') {
|
|
306
|
+
if (pending.direction !== direction) {
|
|
307
|
+
throw new FlexHarnessSessionBusyError(sessionId, 'has a pending opposite reversion operation');
|
|
308
|
+
}
|
|
309
|
+
const target = pending.segmentRunIds
|
|
310
|
+
.map((runId) => stored.reversionSegments.find((segment) => segment.runId === runId))
|
|
311
|
+
.find((segment) => segment?.status === 'completed');
|
|
312
|
+
if (!target) throw new FlexHarnessValidationError('Pending reversion has no target candidate.');
|
|
313
|
+
const closePublication = this.core.openPublication(state, sessionId);
|
|
314
|
+
try {
|
|
315
|
+
await this.resumePendingApply(state, stored, scopeId, scope, operationSignal);
|
|
316
|
+
this.core.emitEvent(scopeId, stored.session, {
|
|
317
|
+
type: 'session.history.changed',
|
|
318
|
+
direction,
|
|
319
|
+
runId: target.runId,
|
|
320
|
+
});
|
|
321
|
+
} finally {
|
|
322
|
+
closePublication();
|
|
323
|
+
}
|
|
324
|
+
return target.runId;
|
|
325
|
+
}
|
|
326
|
+
const unit = this.reversionUnit(stored, direction);
|
|
327
|
+
if (!unit) {
|
|
328
|
+
throw new FlexHarnessSlashCommandUnavailableError(
|
|
329
|
+
direction,
|
|
330
|
+
direction === 'undo' ? 'No visible turn can be undone.' : 'No hidden turn can be redone.',
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (!unit.target.contextAvailable) {
|
|
334
|
+
throw new FlexHarnessSlashCommandUnavailableError(
|
|
335
|
+
direction,
|
|
336
|
+
'The turn is beyond the context archive horizon.',
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
if (this.core.turnReversionProvider && unit.segments.some((segment) =>
|
|
340
|
+
segment.disposition === 'revertible' && segment.workspaceReference === undefined)) {
|
|
341
|
+
throw new FlexHarnessSlashCommandUnavailableError(
|
|
342
|
+
direction,
|
|
343
|
+
'The turn has no available workspace reversion capture.',
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
await this.core.mutateProjection(state, stored, () => {
|
|
347
|
+
// The write-ahead record commits at exactly projectionRevision + 1, so the
|
|
348
|
+
// revision it replaces makes this apply's ID unique. Retries and recovery
|
|
349
|
+
// read the persisted ID instead of deriving it again.
|
|
350
|
+
const operationId = this.reversionId(
|
|
351
|
+
'apply',
|
|
352
|
+
state.storageKey,
|
|
353
|
+
sessionId,
|
|
354
|
+
JSON.stringify([
|
|
355
|
+
direction,
|
|
356
|
+
stored.revertCursor,
|
|
357
|
+
unit.toCursor,
|
|
358
|
+
unit.target.runId,
|
|
359
|
+
stored.projectionRevision,
|
|
360
|
+
]),
|
|
361
|
+
);
|
|
362
|
+
stored.pendingReversion = {
|
|
363
|
+
kind: 'apply',
|
|
364
|
+
operationId,
|
|
365
|
+
direction,
|
|
366
|
+
fromCursor: stored.revertCursor,
|
|
367
|
+
toCursor: unit.toCursor,
|
|
368
|
+
segmentRunIds: unit.segments.map((segment) => segment.runId),
|
|
369
|
+
appliedRunIds: [],
|
|
370
|
+
};
|
|
371
|
+
}, true);
|
|
372
|
+
const closePublication = this.core.openPublication(state, sessionId);
|
|
373
|
+
try {
|
|
374
|
+
await this.resumePendingApply(
|
|
375
|
+
state,
|
|
376
|
+
stored,
|
|
377
|
+
scopeId,
|
|
378
|
+
scope,
|
|
379
|
+
operationSignal,
|
|
380
|
+
);
|
|
381
|
+
this.core.emitEvent(scopeId, stored.session, {
|
|
382
|
+
type: 'session.history.changed',
|
|
383
|
+
direction,
|
|
384
|
+
runId: unit.target.runId,
|
|
385
|
+
});
|
|
386
|
+
} finally {
|
|
387
|
+
closePublication();
|
|
388
|
+
}
|
|
389
|
+
return unit.target.runId;
|
|
390
|
+
},
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
public async reconcileContextAvailability(
|
|
395
|
+
state: IStorageState,
|
|
396
|
+
stored: IStoredSessionState,
|
|
397
|
+
): Promise<boolean> {
|
|
398
|
+
const activeEvents = new Set(stored.agentSession.getEvents().map((event) => event.id));
|
|
399
|
+
if (!stored.reversionSegments.some((segment) =>
|
|
400
|
+
segment.contextAvailable
|
|
401
|
+
&& segment.eventIds.length > 0
|
|
402
|
+
&& !segment.eventIds.some((eventId) => activeEvents.has(eventId)))) return false;
|
|
403
|
+
let branchCommitted = false;
|
|
404
|
+
await this.core.mutateProjection(state, stored, () => {
|
|
405
|
+
for (const segment of stored.reversionSegments) {
|
|
406
|
+
if (
|
|
407
|
+
segment.contextAvailable
|
|
408
|
+
&& segment.eventIds.length > 0
|
|
409
|
+
&& !segment.eventIds.some((eventId) => activeEvents.has(eventId))
|
|
410
|
+
) segment.contextAvailable = false;
|
|
411
|
+
}
|
|
412
|
+
branchCommitted = this.commitArchivedHiddenBranchState(stored);
|
|
413
|
+
this.pruneReversionState(stored, false, true);
|
|
414
|
+
}, true);
|
|
415
|
+
const context = stored.compactorContext as IFlexAgentContextInvocation<TScope> | undefined;
|
|
416
|
+
if (context) {
|
|
417
|
+
await this.drainReversionReleases(state, stored, context.scopeId, context.scope, false);
|
|
418
|
+
}
|
|
419
|
+
return branchCommitted;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
public async resumePendingApply(
|
|
423
|
+
state: IStorageState,
|
|
424
|
+
stored: IStoredSessionState,
|
|
425
|
+
scopeId: string,
|
|
426
|
+
scope: TScope,
|
|
427
|
+
signal?: AbortSignal,
|
|
428
|
+
): Promise<void> {
|
|
429
|
+
const pending = stored.pendingReversion;
|
|
430
|
+
if (pending?.kind !== 'apply') return;
|
|
431
|
+
const sessionGeneration = requireSessionGeneration(stored.session);
|
|
432
|
+
const segmentMap = new Map(stored.reversionSegments.map((segment) => [segment.runId, segment]));
|
|
433
|
+
if (
|
|
434
|
+
pending.segmentRunIds.some((runId) =>
|
|
435
|
+
segmentMap.get(runId)?.disposition === 'revertible')
|
|
436
|
+
&& !this.core.turnReversionProvider
|
|
437
|
+
) {
|
|
438
|
+
throw new FlexHarnessValidationError(
|
|
439
|
+
'Pending workspace reversion recovery requires its turn reversion provider.',
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
const orderedRunIds = pending.direction === 'undo'
|
|
443
|
+
? [...pending.segmentRunIds].reverse()
|
|
444
|
+
: [...pending.segmentRunIds];
|
|
445
|
+
let workspaceProgress = pending.appliedRunIds.some((runId) =>
|
|
446
|
+
segmentMap.get(runId)?.disposition === 'revertible');
|
|
447
|
+
for (const runId of orderedRunIds) {
|
|
448
|
+
if (pending.appliedRunIds.includes(runId)) continue;
|
|
449
|
+
if (signal?.aborted && !workspaceProgress) {
|
|
450
|
+
await this.persistKnownNotApplied(state, stored, pending.operationId, runId);
|
|
451
|
+
throw signal.reason;
|
|
452
|
+
}
|
|
453
|
+
const segment = segmentMap.get(runId);
|
|
454
|
+
if (!segment) throw new FlexHarnessValidationError('Pending reversion references a missing segment.');
|
|
455
|
+
if (segment.disposition === 'revertible') {
|
|
456
|
+
if (!this.core.turnReversionProvider || !segment.captureId || segment.workspaceReference === undefined) {
|
|
457
|
+
throw new FlexHarnessValidationError('Pending workspace reversion segment is incomplete.');
|
|
458
|
+
}
|
|
459
|
+
if (this.reversionProviderProtocolVersion() !== segment.protocolVersion) {
|
|
460
|
+
throw new FlexHarnessValidationError(
|
|
461
|
+
`Pending workspace reversion requires protocolVersion ${segment.protocolVersion}.`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
const captureId = segment.captureId;
|
|
465
|
+
const reference = segment.workspaceReference;
|
|
466
|
+
const childOperationId = `${pending.operationId}:${sha256Hex(runId).slice(0, 16)}`;
|
|
467
|
+
const createContext = (contextSignal: AbortSignal) => Object.freeze({
|
|
468
|
+
scopeId,
|
|
469
|
+
scope,
|
|
470
|
+
storageKey: state.storageKey,
|
|
471
|
+
sessionId: stored.session.sessionId,
|
|
472
|
+
...sessionGeneration,
|
|
473
|
+
runId,
|
|
474
|
+
captureId,
|
|
475
|
+
reference: cloneSerializable(reference),
|
|
476
|
+
operationId: childOperationId,
|
|
477
|
+
direction: pending.direction,
|
|
478
|
+
signal: contextSignal,
|
|
479
|
+
});
|
|
480
|
+
let inspection;
|
|
481
|
+
try {
|
|
482
|
+
inspection = await this.withReversionMaintenanceSignal((inspectionSignal) =>
|
|
483
|
+
this.core.turnReversionProvider!.inspectApply(createContext(inspectionSignal)));
|
|
484
|
+
} catch (inspectionError) {
|
|
485
|
+
state.lifecycle = 'fenced';
|
|
486
|
+
throw this.core.projectOperationError(
|
|
487
|
+
inspectionError,
|
|
488
|
+
'turnReversion',
|
|
489
|
+
scopeId,
|
|
490
|
+
stored.session.sessionId,
|
|
491
|
+
childOperationId,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
if (inspection.status === 'unknown') {
|
|
495
|
+
state.lifecycle = 'fenced';
|
|
496
|
+
throw new FlexHarnessValidationError('Workspace reversion apply outcome is unknown.');
|
|
497
|
+
}
|
|
498
|
+
if (inspection.status === 'not-applied') {
|
|
499
|
+
try {
|
|
500
|
+
if (signal && !workspaceProgress) {
|
|
501
|
+
await this.core.turnReversionProvider.apply(createContext(signal));
|
|
502
|
+
} else {
|
|
503
|
+
await this.withReversionMaintenanceSignal((maintenanceSignal) =>
|
|
504
|
+
this.core.turnReversionProvider!.apply(createContext(maintenanceSignal)));
|
|
505
|
+
}
|
|
506
|
+
} catch (error) {
|
|
507
|
+
try {
|
|
508
|
+
inspection = await this.withReversionMaintenanceSignal((inspectionSignal) =>
|
|
509
|
+
this.core.turnReversionProvider!.inspectApply(createContext(inspectionSignal)));
|
|
510
|
+
} catch (inspectionError) {
|
|
511
|
+
state.lifecycle = 'fenced';
|
|
512
|
+
throw this.core.projectOperationError(
|
|
513
|
+
inspectionError,
|
|
514
|
+
'turnReversion',
|
|
515
|
+
scopeId,
|
|
516
|
+
stored.session.sessionId,
|
|
517
|
+
childOperationId,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
if (inspection.status !== 'applied') {
|
|
521
|
+
if (inspection.status === 'unknown') state.lifecycle = 'fenced';
|
|
522
|
+
else await this.persistKnownNotApplied(state, stored, pending.operationId, runId);
|
|
523
|
+
throw this.core.projectOperationError(
|
|
524
|
+
error,
|
|
525
|
+
'turnReversion',
|
|
526
|
+
scopeId,
|
|
527
|
+
stored.session.sessionId,
|
|
528
|
+
childOperationId,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
await this.core.mutateProjection(state, stored, () => {
|
|
535
|
+
const current = stored.pendingReversion;
|
|
536
|
+
if (current?.kind !== 'apply' || current.operationId !== pending.operationId) {
|
|
537
|
+
throw new FlexHarnessValidationError('Pending reversion changed while applying.');
|
|
538
|
+
}
|
|
539
|
+
if (!current.appliedRunIds.includes(runId)) current.appliedRunIds.push(runId);
|
|
540
|
+
}, true);
|
|
541
|
+
if (segment.disposition === 'revertible') workspaceProgress = true;
|
|
542
|
+
}
|
|
543
|
+
await this.core.mutateProjection(state, stored, () => {
|
|
544
|
+
const current = stored.pendingReversion;
|
|
545
|
+
if (current?.kind !== 'apply' || current.operationId !== pending.operationId) {
|
|
546
|
+
throw new FlexHarnessValidationError('Pending reversion changed before cursor commit.');
|
|
547
|
+
}
|
|
548
|
+
stored.revertCursor = current.toCursor;
|
|
549
|
+
delete stored.pendingReversion;
|
|
550
|
+
}, true);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private async persistKnownNotApplied(
|
|
554
|
+
state: IStorageState,
|
|
555
|
+
stored: IStoredSessionState,
|
|
556
|
+
operationId: string,
|
|
557
|
+
runId: string,
|
|
558
|
+
): Promise<void> {
|
|
559
|
+
await this.core.mutateProjection(state, stored, () => {
|
|
560
|
+
const current = stored.pendingReversion;
|
|
561
|
+
if (current?.kind !== 'apply' || current.operationId !== operationId) {
|
|
562
|
+
throw new FlexHarnessValidationError('Pending reversion changed while recording failed apply.');
|
|
563
|
+
}
|
|
564
|
+
current.appliedRunIds = current.appliedRunIds.filter((entry) => entry !== runId);
|
|
565
|
+
const hasWorkspaceProgress = current.appliedRunIds.some((appliedRunId) =>
|
|
566
|
+
stored.reversionSegments.some((segment) =>
|
|
567
|
+
segment.runId === appliedRunId && segment.disposition === 'revertible'));
|
|
568
|
+
if (!hasWorkspaceProgress) {
|
|
569
|
+
stored.revertCursor = current.fromCursor;
|
|
570
|
+
delete stored.pendingReversion;
|
|
571
|
+
}
|
|
572
|
+
}, true);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
public async recoverPendingCapture(
|
|
576
|
+
state: IStorageState,
|
|
577
|
+
stored: IStoredSessionState,
|
|
578
|
+
scopeId: string,
|
|
579
|
+
scope: TScope,
|
|
580
|
+
outcomes: Map<string, TCanonicalOutcome>,
|
|
581
|
+
): Promise<void> {
|
|
582
|
+
const pending = stored.pendingReversion;
|
|
583
|
+
if (pending?.kind !== 'capture' || !this.core.turnReversionProvider) return;
|
|
584
|
+
const segment = stored.reversionSegments.find((entry) => entry.runId === pending.runId);
|
|
585
|
+
if (!segment) throw new FlexHarnessValidationError('Pending capture references a missing segment.');
|
|
586
|
+
if (this.reversionProviderProtocolVersion() !== pending.protocolVersion) {
|
|
587
|
+
throw new FlexHarnessValidationError(
|
|
588
|
+
`Pending capture recovery requires protocolVersion ${pending.protocolVersion}.`,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
const outcome = outcomes.get(pending.runId);
|
|
592
|
+
if (pending.state === 'prepared') {
|
|
593
|
+
await this.core.mutateProjection(state, stored, () => {
|
|
594
|
+
const current = stored.pendingReversion;
|
|
595
|
+
if (current?.kind !== 'capture' || current.captureId !== pending.captureId) {
|
|
596
|
+
throw new FlexHarnessValidationError('Pending capture changed before finalization.');
|
|
597
|
+
}
|
|
598
|
+
current.state = 'finalizing';
|
|
599
|
+
}, true);
|
|
600
|
+
}
|
|
601
|
+
const finalizedOutcome = await this.resolveReversionCaptureOutcome(
|
|
602
|
+
state,
|
|
603
|
+
scopeId,
|
|
604
|
+
scope,
|
|
605
|
+
stored.session.sessionId,
|
|
606
|
+
requireSessionGeneration(stored.session),
|
|
607
|
+
pending.runId,
|
|
608
|
+
pending.captureId,
|
|
609
|
+
pending.state === 'prepared' ? 'finalizing' : pending.state,
|
|
610
|
+
);
|
|
611
|
+
if (finalizedOutcome === undefined) {
|
|
612
|
+
await this.core.mutateProjection(state, stored, () => {
|
|
613
|
+
stored.reversionSegments = stored.reversionSegments.filter(
|
|
614
|
+
(entry) => entry.runId !== pending.runId,
|
|
615
|
+
);
|
|
616
|
+
delete stored.pendingReversion;
|
|
617
|
+
}, true);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
await this.core.mutateProjection(state, stored, () => {
|
|
621
|
+
segment.status = outcome === 'accepted'
|
|
622
|
+
? 'completed'
|
|
623
|
+
: outcome === 'rejected'
|
|
624
|
+
? 'failed'
|
|
625
|
+
: 'cancelled';
|
|
626
|
+
this.applyFinalizedReversionOutcome(stored, segment, finalizedOutcome);
|
|
627
|
+
segment.eventIds = stored.agentSession.getEvents()
|
|
628
|
+
.filter((event) => event.generationId === pending.runId)
|
|
629
|
+
.map((event) => event.id);
|
|
630
|
+
if (segment.status === 'completed') stored.revertCursor++;
|
|
631
|
+
if (outcome === undefined && !stored.excludedRunIds.includes(pending.runId)) {
|
|
632
|
+
stored.excludedRunIds.push(pending.runId);
|
|
633
|
+
}
|
|
634
|
+
delete stored.pendingReversion;
|
|
635
|
+
this.pruneReversionState(stored, false, true);
|
|
636
|
+
}, true);
|
|
637
|
+
await this.drainReversionReleases(state, stored, scopeId, scope, false);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
public reversionId(
|
|
641
|
+
kind: 'capture' | 'apply',
|
|
642
|
+
storageKey: string,
|
|
643
|
+
sessionId: string,
|
|
644
|
+
value: string,
|
|
645
|
+
): string {
|
|
646
|
+
return `${kind}_${sha256Hex(JSON.stringify([storageKey, sessionId, value]))}`;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
public reversionProviderProtocolVersion(): 1 | 2 | undefined {
|
|
650
|
+
if (!this.core.turnReversionProvider) return undefined;
|
|
651
|
+
return 'protocolVersion' in this.core.turnReversionProvider ? 2 : 1;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private reversionCaptureContext(
|
|
655
|
+
run: IActiveRun,
|
|
656
|
+
captureId: string,
|
|
657
|
+
signal: AbortSignal,
|
|
658
|
+
) {
|
|
659
|
+
return Object.freeze({
|
|
660
|
+
scopeId: run.scopeId,
|
|
661
|
+
scope: run.scope as TScope,
|
|
662
|
+
storageKey: run.state.storageKey,
|
|
663
|
+
sessionId: run.sessionId,
|
|
664
|
+
...requireSessionGeneration(run.stored.session),
|
|
665
|
+
runId: run.runId,
|
|
666
|
+
captureId,
|
|
667
|
+
signal,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private async withReversionMaintenanceSignal<TResult>(
|
|
672
|
+
operation: (signal: AbortSignal) => Promise<TResult> | TResult,
|
|
673
|
+
): Promise<TResult> {
|
|
674
|
+
const controller = new AbortController();
|
|
675
|
+
const timeoutMs = this.core.reversionMaintenanceTimeoutMs;
|
|
676
|
+
const timeoutError = new Error(`Turn reversion maintenance timed out after ${timeoutMs}ms.`);
|
|
677
|
+
let rejectTimeout!: (error: Error) => void;
|
|
678
|
+
const timeout = new Promise<never>((_resolve, reject) => {
|
|
679
|
+
rejectTimeout = reject;
|
|
680
|
+
});
|
|
681
|
+
const timer = setTimeout(() => {
|
|
682
|
+
controller.abort(timeoutError);
|
|
683
|
+
rejectTimeout(timeoutError);
|
|
684
|
+
}, timeoutMs);
|
|
685
|
+
const completion = Promise.resolve().then(() => operation(controller.signal));
|
|
686
|
+
void completion.catch(() => undefined);
|
|
687
|
+
try {
|
|
688
|
+
return await Promise.race([completion, timeout]);
|
|
689
|
+
} finally {
|
|
690
|
+
clearTimeout(timer);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private inspectReversionCapture(
|
|
695
|
+
scopeId: string,
|
|
696
|
+
scope: TScope,
|
|
697
|
+
storageKey: string,
|
|
698
|
+
sessionId: string,
|
|
699
|
+
sessionGeneration: Readonly<IFlexSessionGeneration>,
|
|
700
|
+
runId: string,
|
|
701
|
+
captureId: string,
|
|
702
|
+
): Promise<TNormalizedCaptureInspection> {
|
|
703
|
+
return this.withReversionMaintenanceSignal(async (signal) => {
|
|
704
|
+
const provider = this.core.turnReversionProvider!;
|
|
705
|
+
const inspection = await provider.inspectCapture(Object.freeze({
|
|
706
|
+
scopeId,
|
|
707
|
+
scope,
|
|
708
|
+
storageKey,
|
|
709
|
+
sessionId,
|
|
710
|
+
...sessionGeneration,
|
|
711
|
+
runId,
|
|
712
|
+
captureId,
|
|
713
|
+
signal,
|
|
714
|
+
}));
|
|
715
|
+
if (inspection.status !== 'finalized') return { status: inspection.status };
|
|
716
|
+
if (this.reversionProviderProtocolVersion() === 2) {
|
|
717
|
+
if (!('outcome' in inspection)) {
|
|
718
|
+
throw new FlexHarnessValidationError('Protocol-2 finalized inspection has no outcome.');
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
status: 'finalized',
|
|
722
|
+
outcome: this.normalizeReversionOutcome(inspection.outcome),
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
if (!('reference' in inspection) || inspection.reference === undefined) {
|
|
726
|
+
throw new FlexHarnessValidationError('Protocol-1 finalized inspection has no reference.');
|
|
727
|
+
}
|
|
728
|
+
return {
|
|
729
|
+
status: 'finalized',
|
|
730
|
+
outcome: {
|
|
731
|
+
disposition: 'revertible',
|
|
732
|
+
reference: this.normalizeReversionReference(inspection.reference),
|
|
733
|
+
affectedWorkspaces: [],
|
|
734
|
+
},
|
|
735
|
+
};
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private finalizeReversionCapture(
|
|
740
|
+
scopeId: string,
|
|
741
|
+
scope: TScope,
|
|
742
|
+
storageKey: string,
|
|
743
|
+
sessionId: string,
|
|
744
|
+
sessionGeneration: Readonly<IFlexSessionGeneration>,
|
|
745
|
+
runId: string,
|
|
746
|
+
captureId: string,
|
|
747
|
+
): Promise<TFlexTurnReversionFinalizedOutcome> {
|
|
748
|
+
return this.withReversionMaintenanceSignal(async (signal) => {
|
|
749
|
+
const result = await this.core.turnReversionProvider!.finalize(Object.freeze({
|
|
750
|
+
scopeId,
|
|
751
|
+
scope,
|
|
752
|
+
storageKey,
|
|
753
|
+
sessionId,
|
|
754
|
+
...sessionGeneration,
|
|
755
|
+
runId,
|
|
756
|
+
captureId,
|
|
757
|
+
signal,
|
|
758
|
+
}));
|
|
759
|
+
return this.reversionProviderProtocolVersion() === 2
|
|
760
|
+
? this.normalizeReversionOutcome(result)
|
|
761
|
+
: {
|
|
762
|
+
disposition: 'revertible',
|
|
763
|
+
reference: this.normalizeReversionReference(result),
|
|
764
|
+
affectedWorkspaces: [],
|
|
765
|
+
};
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private async resolveReversionCaptureOutcome(
|
|
770
|
+
state: IStorageState,
|
|
771
|
+
scopeId: string,
|
|
772
|
+
scope: TScope,
|
|
773
|
+
sessionId: string,
|
|
774
|
+
sessionGeneration: Readonly<IFlexSessionGeneration>,
|
|
775
|
+
runId: string,
|
|
776
|
+
captureId: string,
|
|
777
|
+
durableState: 'preparing' | 'prepared' | 'finalizing',
|
|
778
|
+
): Promise<TFlexTurnReversionFinalizedOutcome | undefined> {
|
|
779
|
+
let inspection;
|
|
780
|
+
try {
|
|
781
|
+
inspection = await this.inspectReversionCapture(
|
|
782
|
+
scopeId,
|
|
783
|
+
scope,
|
|
784
|
+
state.storageKey,
|
|
785
|
+
sessionId,
|
|
786
|
+
sessionGeneration,
|
|
787
|
+
runId,
|
|
788
|
+
captureId,
|
|
789
|
+
);
|
|
790
|
+
} catch (error) {
|
|
791
|
+
state.lifecycle = 'fenced';
|
|
792
|
+
throw this.core.projectOperationError(error, 'turnReversion', scopeId, sessionId, captureId);
|
|
793
|
+
}
|
|
794
|
+
if (inspection.status === 'unknown') {
|
|
795
|
+
state.lifecycle = 'fenced';
|
|
796
|
+
throw new FlexHarnessValidationError('Reversion capture outcome is unknown.');
|
|
797
|
+
}
|
|
798
|
+
if (inspection.status === 'missing') {
|
|
799
|
+
if (durableState === 'preparing') return undefined;
|
|
800
|
+
state.lifecycle = 'fenced';
|
|
801
|
+
throw new FlexHarnessValidationError('Prepared reversion capture is missing.');
|
|
802
|
+
}
|
|
803
|
+
let outcome = inspection.status === 'finalized' ? inspection.outcome : undefined;
|
|
804
|
+
if (inspection.status === 'prepared') {
|
|
805
|
+
try {
|
|
806
|
+
outcome = await this.finalizeReversionCapture(
|
|
807
|
+
scopeId,
|
|
808
|
+
scope,
|
|
809
|
+
state.storageKey,
|
|
810
|
+
sessionId,
|
|
811
|
+
sessionGeneration,
|
|
812
|
+
runId,
|
|
813
|
+
captureId,
|
|
814
|
+
);
|
|
815
|
+
} catch (error) {
|
|
816
|
+
const finalInspection = await this.inspectReversionCapture(
|
|
817
|
+
scopeId,
|
|
818
|
+
scope,
|
|
819
|
+
state.storageKey,
|
|
820
|
+
sessionId,
|
|
821
|
+
sessionGeneration,
|
|
822
|
+
runId,
|
|
823
|
+
captureId,
|
|
824
|
+
);
|
|
825
|
+
if (finalInspection.status !== 'finalized') {
|
|
826
|
+
if (finalInspection.status === 'unknown') state.lifecycle = 'fenced';
|
|
827
|
+
throw this.core.projectOperationError(error, 'turnReversion', scopeId, sessionId, captureId);
|
|
828
|
+
}
|
|
829
|
+
outcome = finalInspection.outcome;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (outcome === undefined) {
|
|
833
|
+
state.lifecycle = 'fenced';
|
|
834
|
+
throw new FlexHarnessValidationError('Finalized reversion capture has no outcome.');
|
|
835
|
+
}
|
|
836
|
+
return outcome;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private normalizeReversionReference(reference: unknown): TJsonValue {
|
|
840
|
+
return normalizeJsonValue(reference, {
|
|
841
|
+
maxDepth: this.core.toolOutputLimits.maxDepth,
|
|
842
|
+
maxBytes: Math.min(
|
|
843
|
+
this.core.toolOutputLimits.maxBytes,
|
|
844
|
+
FLEX_REVERSION_REFERENCE_MAX_BYTES,
|
|
845
|
+
),
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private normalizeReversionOutcome(value: unknown): TFlexTurnReversionFinalizedOutcome {
|
|
850
|
+
if (
|
|
851
|
+
!value
|
|
852
|
+
|| typeof value !== 'object'
|
|
853
|
+
|| Array.isArray(value)
|
|
854
|
+
|| (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
855
|
+
) throw new FlexHarnessValidationError('Turn reversion finalized outcome must be a plain object.');
|
|
856
|
+
const outcome = value as Record<string, unknown>;
|
|
857
|
+
const disposition = outcome.disposition;
|
|
858
|
+
if (!['revertible', 'no-change', 'nonrevertible'].includes(String(disposition))) {
|
|
859
|
+
throw new FlexHarnessValidationError('Turn reversion finalized outcome disposition is invalid.');
|
|
860
|
+
}
|
|
861
|
+
const allowedKeys = disposition === 'nonrevertible'
|
|
862
|
+
? ['disposition', 'reference', 'reasonCode', 'affectedWorkspaces']
|
|
863
|
+
: ['disposition', 'reference', 'affectedWorkspaces'];
|
|
864
|
+
const unsupported = Object.keys(outcome).find((key) => !allowedKeys.includes(key));
|
|
865
|
+
if (unsupported) {
|
|
866
|
+
throw new FlexHarnessValidationError(
|
|
867
|
+
`Turn reversion finalized outcome does not support "${unsupported}".`,
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
if (!Object.prototype.hasOwnProperty.call(outcome, 'reference')) {
|
|
871
|
+
throw new FlexHarnessValidationError('Turn reversion finalized outcome requires a reference.');
|
|
872
|
+
}
|
|
873
|
+
const reference = this.normalizeReversionReference(outcome.reference);
|
|
874
|
+
const affectedWorkspaces = outcome.affectedWorkspaces === undefined
|
|
875
|
+
? undefined
|
|
876
|
+
: this.normalizeAffectedWorkspaces(outcome.affectedWorkspaces);
|
|
877
|
+
if (disposition === 'revertible') {
|
|
878
|
+
if (affectedWorkspaces === undefined) {
|
|
879
|
+
throw new FlexHarnessValidationError('Revertible outcome requires affectedWorkspaces.');
|
|
880
|
+
}
|
|
881
|
+
return { disposition, reference, affectedWorkspaces };
|
|
882
|
+
}
|
|
883
|
+
if (disposition === 'no-change') {
|
|
884
|
+
if (affectedWorkspaces && affectedWorkspaces.length > 0) {
|
|
885
|
+
throw new FlexHarnessValidationError('No-change outcome cannot affect a workspace.');
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
disposition,
|
|
889
|
+
reference,
|
|
890
|
+
...(affectedWorkspaces === undefined ? {} : { affectedWorkspaces: [] }),
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
if (
|
|
894
|
+
typeof outcome.reasonCode !== 'string'
|
|
895
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(outcome.reasonCode)
|
|
896
|
+
|| Buffer.byteLength(outcome.reasonCode, 'utf8') > FLEX_REVERSION_REASON_CODE_MAX_BYTES
|
|
897
|
+
) throw new FlexHarnessValidationError('Nonrevertible outcome reasonCode is invalid.');
|
|
898
|
+
return {
|
|
899
|
+
disposition: 'nonrevertible',
|
|
900
|
+
reference,
|
|
901
|
+
reasonCode: outcome.reasonCode,
|
|
902
|
+
...(affectedWorkspaces === undefined ? {} : { affectedWorkspaces }),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
private normalizeAffectedWorkspaces(value: unknown): IFlexAffectedWorkspace[] {
|
|
907
|
+
if (!Array.isArray(value) || value.length > FLEX_REVERSION_MAX_AFFECTED_WORKSPACES) {
|
|
908
|
+
throw new FlexHarnessValidationError(
|
|
909
|
+
`affectedWorkspaces must contain at most ${FLEX_REVERSION_MAX_AFFECTED_WORKSPACES} entries.`,
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
const ids = new Set<string>();
|
|
913
|
+
return value.map((entry, index) => {
|
|
914
|
+
if (
|
|
915
|
+
!entry
|
|
916
|
+
|| typeof entry !== 'object'
|
|
917
|
+
|| Array.isArray(entry)
|
|
918
|
+
|| (Object.getPrototypeOf(entry) !== Object.prototype && Object.getPrototypeOf(entry) !== null)
|
|
919
|
+
) throw new FlexHarnessValidationError(`affectedWorkspaces[${index}] must be a plain object.`);
|
|
920
|
+
const descriptor = entry as Record<string, unknown>;
|
|
921
|
+
const unsupported = Object.keys(descriptor).find((key) => !['id', 'label'].includes(key));
|
|
922
|
+
if (unsupported) {
|
|
923
|
+
throw new FlexHarnessValidationError(
|
|
924
|
+
`affectedWorkspaces[${index}] does not support "${unsupported}".`,
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
if (
|
|
928
|
+
typeof descriptor.id !== 'string'
|
|
929
|
+
|| !descriptor.id.trim()
|
|
930
|
+
|| Buffer.byteLength(descriptor.id, 'utf8') > FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES
|
|
931
|
+
) throw new FlexHarnessValidationError(`affectedWorkspaces[${index}].id is invalid.`);
|
|
932
|
+
if (
|
|
933
|
+
typeof descriptor.label !== 'string'
|
|
934
|
+
|| !descriptor.label.trim()
|
|
935
|
+
|| Buffer.byteLength(descriptor.label, 'utf8') > FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES
|
|
936
|
+
) throw new FlexHarnessValidationError(`affectedWorkspaces[${index}].label is invalid.`);
|
|
937
|
+
if (ids.has(descriptor.id)) {
|
|
938
|
+
throw new FlexHarnessValidationError(`affectedWorkspaces contains duplicate id "${descriptor.id}".`);
|
|
939
|
+
}
|
|
940
|
+
ids.add(descriptor.id);
|
|
941
|
+
return { id: descriptor.id, label: descriptor.label };
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
public async prepareRunReversion(run: IActiveRun, signal: AbortSignal): Promise<void> {
|
|
946
|
+
if (run.stored.session.agent !== undefined || !this.core.turnReversionProvider) return;
|
|
947
|
+
const segment = run.stored.reversionSegments.find((entry) => entry.runId === run.runId);
|
|
948
|
+
const captureId = segment?.captureId;
|
|
949
|
+
if (!segment || !captureId) {
|
|
950
|
+
throw this.core.projectExternalError(
|
|
951
|
+
run,
|
|
952
|
+
new Error('Root generation is missing its durable reversion capture intent.'),
|
|
953
|
+
'turnReversion',
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
const context = this.reversionCaptureContext(run, captureId, signal);
|
|
957
|
+
if (this.reversionProviderProtocolVersion() !== segment.protocolVersion) {
|
|
958
|
+
throw this.core.projectExternalError(
|
|
959
|
+
run,
|
|
960
|
+
new Error(`Root generation requires reversion protocolVersion ${segment.protocolVersion}.`),
|
|
961
|
+
'turnReversion',
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
try {
|
|
965
|
+
await this.core.turnReversionProvider.prepare(context);
|
|
966
|
+
} catch (error) {
|
|
967
|
+
let inspection;
|
|
968
|
+
try {
|
|
969
|
+
inspection = await this.inspectReversionCapture(
|
|
970
|
+
run.scopeId,
|
|
971
|
+
run.scope as TScope,
|
|
972
|
+
run.state.storageKey,
|
|
973
|
+
run.sessionId,
|
|
974
|
+
requireSessionGeneration(run.stored.session),
|
|
975
|
+
run.runId,
|
|
976
|
+
captureId,
|
|
977
|
+
);
|
|
978
|
+
} catch (inspectionError) {
|
|
979
|
+
run.state.lifecycle = 'fenced';
|
|
980
|
+
throw this.core.projectExternalError(run, inspectionError, 'turnReversion');
|
|
981
|
+
}
|
|
982
|
+
if (inspection.status !== 'prepared' && inspection.status !== 'finalized') {
|
|
983
|
+
if (inspection.status === 'unknown') run.state.lifecycle = 'fenced';
|
|
984
|
+
throw this.core.projectExternalError(run, error, 'turnReversion');
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
988
|
+
const pending = run.stored.pendingReversion;
|
|
989
|
+
if (pending?.kind !== 'capture' || pending.runId !== run.runId) {
|
|
990
|
+
throw new FlexHarnessValidationError('Reversion capture intent changed during preparation.');
|
|
991
|
+
}
|
|
992
|
+
pending.state = 'prepared';
|
|
993
|
+
}, true);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
public async finalizeRunReversion(
|
|
997
|
+
run: IActiveRun,
|
|
998
|
+
status: 'completed' | 'failed' | 'cancelled',
|
|
999
|
+
): Promise<void> {
|
|
1000
|
+
if (run.stored.session.agent !== undefined) return;
|
|
1001
|
+
const segment = run.stored.reversionSegments.find((entry) => entry.runId === run.runId);
|
|
1002
|
+
if (!segment) return;
|
|
1003
|
+
const eventIds = run.stored.agentSession.getEvents()
|
|
1004
|
+
.filter((event) => event.generationId === run.runId)
|
|
1005
|
+
.map((event) => event.id);
|
|
1006
|
+
if (!this.core.turnReversionProvider || segment.provenance === 'transcript') {
|
|
1007
|
+
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
1008
|
+
const wasCompleted = segment.status === 'completed';
|
|
1009
|
+
segment.status = status;
|
|
1010
|
+
segment.eventIds = eventIds;
|
|
1011
|
+
if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
|
|
1012
|
+
else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
|
|
1013
|
+
delete run.stored.pendingReversion;
|
|
1014
|
+
this.pruneReversionState(run.stored);
|
|
1015
|
+
}, true);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (!segment.captureId) {
|
|
1019
|
+
throw new FlexHarnessValidationError('Workspace reversion segment has no capture ownership.');
|
|
1020
|
+
}
|
|
1021
|
+
const captureId = segment.captureId;
|
|
1022
|
+
if (segment.workspaceReference !== undefined) {
|
|
1023
|
+
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
1024
|
+
const wasCompleted = segment.status === 'completed';
|
|
1025
|
+
segment.status = status;
|
|
1026
|
+
segment.eventIds = eventIds;
|
|
1027
|
+
if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
|
|
1028
|
+
else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
|
|
1029
|
+
delete run.stored.pendingReversion;
|
|
1030
|
+
}, true);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const pending = run.stored.pendingReversion;
|
|
1034
|
+
if (pending?.kind !== 'capture' || pending.runId !== run.runId) {
|
|
1035
|
+
throw new FlexHarnessValidationError('Reversion capture intent changed before finalization.');
|
|
1036
|
+
}
|
|
1037
|
+
if (this.reversionProviderProtocolVersion() !== pending.protocolVersion) {
|
|
1038
|
+
throw new FlexHarnessValidationError(
|
|
1039
|
+
`Reversion capture finalization requires protocolVersion ${pending.protocolVersion}.`,
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
if (pending.state === 'prepared') {
|
|
1043
|
+
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
1044
|
+
const current = run.stored.pendingReversion;
|
|
1045
|
+
if (current?.kind !== 'capture' || current.captureId !== captureId) {
|
|
1046
|
+
throw new FlexHarnessValidationError('Reversion capture changed before finalization.');
|
|
1047
|
+
}
|
|
1048
|
+
current.state = 'finalizing';
|
|
1049
|
+
}, true);
|
|
1050
|
+
}
|
|
1051
|
+
const outcome = await this.resolveReversionCaptureOutcome(
|
|
1052
|
+
run.state,
|
|
1053
|
+
run.scopeId,
|
|
1054
|
+
run.scope as TScope,
|
|
1055
|
+
run.sessionId,
|
|
1056
|
+
requireSessionGeneration(run.stored.session),
|
|
1057
|
+
run.runId,
|
|
1058
|
+
captureId,
|
|
1059
|
+
pending.state === 'prepared' ? 'finalizing' : pending.state,
|
|
1060
|
+
);
|
|
1061
|
+
if (outcome === undefined) {
|
|
1062
|
+
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
1063
|
+
run.stored.reversionSegments = run.stored.reversionSegments.filter(
|
|
1064
|
+
(entry) => entry.runId !== run.runId,
|
|
1065
|
+
);
|
|
1066
|
+
delete run.stored.pendingReversion;
|
|
1067
|
+
}, true);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
1071
|
+
const current = run.stored.pendingReversion;
|
|
1072
|
+
if (current?.kind !== 'capture' || current.runId !== run.runId) {
|
|
1073
|
+
throw new FlexHarnessValidationError('Reversion capture intent changed before finalization.');
|
|
1074
|
+
}
|
|
1075
|
+
const wasCompleted = segment.status === 'completed';
|
|
1076
|
+
segment.status = status;
|
|
1077
|
+
segment.eventIds = eventIds;
|
|
1078
|
+
this.applyFinalizedReversionOutcome(run.stored, segment, outcome);
|
|
1079
|
+
if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
|
|
1080
|
+
else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
|
|
1081
|
+
delete run.stored.pendingReversion;
|
|
1082
|
+
this.pruneReversionState(run.stored);
|
|
1083
|
+
}, true);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
private applyFinalizedReversionOutcome(
|
|
1087
|
+
stored: IStoredSessionState,
|
|
1088
|
+
segment: IFlexReversionSegment,
|
|
1089
|
+
outcome: TFlexTurnReversionFinalizedOutcome,
|
|
1090
|
+
): void {
|
|
1091
|
+
const captureId = segment.captureId;
|
|
1092
|
+
if (!captureId) {
|
|
1093
|
+
throw new FlexHarnessValidationError('Finalized workspace outcome has no capture ownership.');
|
|
1094
|
+
}
|
|
1095
|
+
segment.disposition = outcome.disposition;
|
|
1096
|
+
delete segment.affectedWorkspaces;
|
|
1097
|
+
delete segment.reasonCode;
|
|
1098
|
+
if (segment.protocolVersion === 2) {
|
|
1099
|
+
if (outcome.affectedWorkspaces !== undefined) {
|
|
1100
|
+
segment.affectedWorkspaces = cloneSerializable([...outcome.affectedWorkspaces]);
|
|
1101
|
+
}
|
|
1102
|
+
if (outcome.disposition === 'nonrevertible') segment.reasonCode = outcome.reasonCode;
|
|
1103
|
+
}
|
|
1104
|
+
if (outcome.disposition === 'revertible') {
|
|
1105
|
+
segment.workspaceReference = cloneSerializable(outcome.reference);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
this.enqueueReversionReleaseReference(stored, {
|
|
1109
|
+
runId: segment.runId,
|
|
1110
|
+
captureId,
|
|
1111
|
+
reference: cloneSerializable(outcome.reference),
|
|
1112
|
+
protocolVersion: segment.protocolVersion,
|
|
1113
|
+
});
|
|
1114
|
+
delete segment.captureId;
|
|
1115
|
+
delete segment.workspaceReference;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
private hiddenReversionSegments(stored: IStoredSessionState): IFlexReversionSegment[] {
|
|
1119
|
+
const firstHidden = this.reversionGroups(stored)[stored.revertCursor]?.segments[0];
|
|
1120
|
+
if (!firstHidden) return [];
|
|
1121
|
+
const index = stored.reversionSegments.findIndex((segment) => segment.runId === firstHidden.runId);
|
|
1122
|
+
if (index < 0) return [];
|
|
1123
|
+
return stored.reversionSegments.slice(index);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
private enqueueReversionRelease(
|
|
1127
|
+
stored: IStoredSessionState,
|
|
1128
|
+
segment: IFlexReversionSegment,
|
|
1129
|
+
): void {
|
|
1130
|
+
if (segment.disposition !== 'revertible') return;
|
|
1131
|
+
if (segment.captureId === undefined || segment.workspaceReference === undefined) {
|
|
1132
|
+
throw new FlexHarnessValidationError('Capture-backed segment has no releasable workspace reference.');
|
|
1133
|
+
}
|
|
1134
|
+
this.enqueueReversionReleaseReference(stored, {
|
|
1135
|
+
runId: segment.runId,
|
|
1136
|
+
captureId: segment.captureId,
|
|
1137
|
+
reference: cloneSerializable(segment.workspaceReference),
|
|
1138
|
+
protocolVersion: segment.protocolVersion,
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
private enqueueReversionReleaseReference(
|
|
1143
|
+
stored: IStoredSessionState,
|
|
1144
|
+
release: IFlexPendingReversionRelease,
|
|
1145
|
+
): void {
|
|
1146
|
+
if (stored.pendingReversionReleases.some((entry) => entry.captureId === release.captureId)) {
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (stored.pendingReversionReleases.length >= this.core.reversionLimits.maxPendingReversionReleases) {
|
|
1150
|
+
throw new FlexHarnessValidationError('Pending reversion release limit was reached.');
|
|
1151
|
+
}
|
|
1152
|
+
stored.pendingReversionReleases.push(cloneSerializable(release));
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
public pruneReversionState(
|
|
1156
|
+
stored: IStoredSessionState,
|
|
1157
|
+
reserveSegment = false,
|
|
1158
|
+
dropNonUndoablePrefix = false,
|
|
1159
|
+
): void {
|
|
1160
|
+
if (stored.pendingReversion || stored.revertCursor !== this.completedReversionCandidates(stored).length) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
while (stored.reversionSegments.length > 0) {
|
|
1164
|
+
const candidates = this.completedReversionCandidates(stored);
|
|
1165
|
+
const overLimit = candidates.length + (reserveSegment ? 1 : 0)
|
|
1166
|
+
> this.core.reversionLimits.maxCompletedTurns
|
|
1167
|
+
|| stored.reversionSegments.length + (reserveSegment ? 1 : 0)
|
|
1168
|
+
> this.core.reversionLimits.maxSegments;
|
|
1169
|
+
const firstCandidate = candidates[0];
|
|
1170
|
+
const nextCandidate = candidates[1];
|
|
1171
|
+
let end = 0;
|
|
1172
|
+
if (!firstCandidate) {
|
|
1173
|
+
if (dropNonUndoablePrefix) end = stored.reversionSegments.length;
|
|
1174
|
+
else if (overLimit) {
|
|
1175
|
+
end = Math.max(
|
|
1176
|
+
1,
|
|
1177
|
+
stored.reversionSegments.length + (reserveSegment ? 1 : 0)
|
|
1178
|
+
- this.core.reversionLimits.maxSegments,
|
|
1179
|
+
);
|
|
1180
|
+
} else break;
|
|
1181
|
+
} else if (!firstCandidate.contextAvailable) {
|
|
1182
|
+
end = nextCandidate
|
|
1183
|
+
? stored.reversionSegments.findIndex((segment) => segment.runId === nextCandidate.runId)
|
|
1184
|
+
: stored.reversionSegments.length;
|
|
1185
|
+
} else if (overLimit) {
|
|
1186
|
+
end = nextCandidate
|
|
1187
|
+
? stored.reversionSegments.findIndex((segment) => segment.runId === nextCandidate.runId)
|
|
1188
|
+
: stored.reversionSegments.length;
|
|
1189
|
+
} else {
|
|
1190
|
+
break;
|
|
1191
|
+
}
|
|
1192
|
+
if (end <= 0) break;
|
|
1193
|
+
const prefix = stored.reversionSegments.slice(0, end);
|
|
1194
|
+
if (prefix.some((segment) =>
|
|
1195
|
+
segment.disposition === 'revertible' && segment.workspaceReference === undefined)) break;
|
|
1196
|
+
for (const segment of prefix) this.enqueueReversionRelease(stored, segment);
|
|
1197
|
+
stored.reversionSegments.splice(0, end);
|
|
1198
|
+
stored.revertCursor = Math.max(
|
|
1199
|
+
0,
|
|
1200
|
+
stored.revertCursor - prefix.filter((segment) => segment.status === 'completed').length,
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
if (stored.excludedRunIds.length > this.core.reversionLimits.maxExcludedRunIds) {
|
|
1204
|
+
throw new FlexHarnessValidationError('Excluded reversion run limit was reached.');
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
public async commitRevertedBranch(
|
|
1209
|
+
state: IStorageState,
|
|
1210
|
+
stored: IStoredSessionState,
|
|
1211
|
+
scopeId: string,
|
|
1212
|
+
scope: TScope,
|
|
1213
|
+
): Promise<void> {
|
|
1214
|
+
if (stored.pendingReversionReleases.length > 0) {
|
|
1215
|
+
await this.drainReversionReleases(state, stored, scopeId, scope, false);
|
|
1216
|
+
this.core.assertStateAcceptingWork(state);
|
|
1217
|
+
}
|
|
1218
|
+
if (this.hiddenReversionSegments(stored).length === 0) {
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
await this.core.mutateProjection(state, stored, () => this.commitRevertedBranchState(stored), true);
|
|
1222
|
+
this.core.emitEvent(scopeId, stored.session, {
|
|
1223
|
+
type: 'session.history.changed',
|
|
1224
|
+
direction: 'branch',
|
|
1225
|
+
});
|
|
1226
|
+
await this.drainReversionReleases(state, stored, scopeId, scope, false);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
public commitRevertedBranchState(stored: IStoredSessionState): boolean {
|
|
1230
|
+
const hidden = this.hiddenReversionSegments(stored);
|
|
1231
|
+
if (hidden.length === 0) return false;
|
|
1232
|
+
const hiddenRunIds = new Set(hidden.map((segment) => segment.runId));
|
|
1233
|
+
stored.messages = stored.messages.filter((message) => !hiddenRunIds.has(message.runId));
|
|
1234
|
+
stored.stagedTerminals = stored.stagedTerminals.filter(
|
|
1235
|
+
(terminal) => !hiddenRunIds.has(terminal.runId),
|
|
1236
|
+
);
|
|
1237
|
+
for (const segment of hidden) {
|
|
1238
|
+
this.enqueueReversionRelease(stored, segment);
|
|
1239
|
+
if (!stored.excludedRunIds.includes(segment.runId)) stored.excludedRunIds.push(segment.runId);
|
|
1240
|
+
}
|
|
1241
|
+
stored.reversionSegments = stored.reversionSegments.filter(
|
|
1242
|
+
(segment) => !hiddenRunIds.has(segment.runId),
|
|
1243
|
+
);
|
|
1244
|
+
stored.revertCursor = this.completedReversionCandidates(stored).length;
|
|
1245
|
+
this.pruneReversionState(stored);
|
|
1246
|
+
return true;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
public commitArchivedHiddenBranchState(stored: IStoredSessionState): boolean {
|
|
1250
|
+
const hidden = this.hiddenReversionSegments(stored);
|
|
1251
|
+
return hidden.some((segment) => !segment.contextAvailable)
|
|
1252
|
+
? this.commitRevertedBranchState(stored)
|
|
1253
|
+
: false;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
public refreshAutomaticArchiveHorizon(stored: IStoredSessionState): void {
|
|
1257
|
+
const events = stored.agentSession.getEvents();
|
|
1258
|
+
const activeIds = new Set(events.map((event) => event.id));
|
|
1259
|
+
const archivedIds = new Set(events.flatMap((event) => event.type === 'context-compaction' && event.automatic
|
|
1260
|
+
? event.coveredEventIds.filter((id) => !activeIds.has(id)) : []));
|
|
1261
|
+
if (archivedIds.size === 0) return;
|
|
1262
|
+
for (const segment of stored.reversionSegments) {
|
|
1263
|
+
if (segment.eventIds.some((id) => archivedIds.has(id))) segment.contextAvailable = false;
|
|
1264
|
+
}
|
|
1265
|
+
this.commitArchivedHiddenBranchState(stored);
|
|
1266
|
+
this.pruneReversionState(stored, false, true);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
public filterReversionContextEvents(
|
|
1270
|
+
stored: IStoredSessionState,
|
|
1271
|
+
events: readonly plugins.TAgentEvent[],
|
|
1272
|
+
): plugins.TAgentEvent[] {
|
|
1273
|
+
const hiddenRunIds = new Set([
|
|
1274
|
+
...this.hiddenReversionSegments(stored).map((segment) => segment.runId),
|
|
1275
|
+
...stored.excludedRunIds,
|
|
1276
|
+
]);
|
|
1277
|
+
const hiddenRawIds = new Set([
|
|
1278
|
+
...this.hiddenReversionSegments(stored).flatMap((segment) => segment.eventIds),
|
|
1279
|
+
...events.filter((event) => event.generationId && hiddenRunIds.has(event.generationId))
|
|
1280
|
+
.map((event) => event.id),
|
|
1281
|
+
]);
|
|
1282
|
+
const taintedCompactionIds = new Set<string>();
|
|
1283
|
+
let changed = true;
|
|
1284
|
+
while (changed) {
|
|
1285
|
+
changed = false;
|
|
1286
|
+
for (const event of events) {
|
|
1287
|
+
if (event.type !== 'context-compaction' || taintedCompactionIds.has(event.id)) continue;
|
|
1288
|
+
if (
|
|
1289
|
+
event.coveredEventIds.some((id) => hiddenRawIds.has(id) || taintedCompactionIds.has(id))
|
|
1290
|
+
|| event.archivedTransactions?.some((transaction) =>
|
|
1291
|
+
hiddenRunIds.has(transaction.generationId))
|
|
1292
|
+
) {
|
|
1293
|
+
taintedCompactionIds.add(event.id);
|
|
1294
|
+
changed = true;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return events.filter((event) =>
|
|
1299
|
+
!(event.generationId && hiddenRunIds.has(event.generationId))
|
|
1300
|
+
&& !taintedCompactionIds.has(event.id));
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
public async drainReversionReleases(
|
|
1304
|
+
state: IStorageState,
|
|
1305
|
+
stored: IStoredSessionState,
|
|
1306
|
+
scopeId: string,
|
|
1307
|
+
scope: TScope,
|
|
1308
|
+
throwOnFailure: boolean,
|
|
1309
|
+
): Promise<void> {
|
|
1310
|
+
await this.core.reconcileProjectionFromStore(stored);
|
|
1311
|
+
if (!this.core.turnReversionProvider) {
|
|
1312
|
+
if (stored.pendingReversionReleases.length > 0) {
|
|
1313
|
+
throw new FlexHarnessValidationError(
|
|
1314
|
+
'Pending workspace capture releases require their turn reversion provider.',
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const sessionGeneration = requireSessionGeneration(stored.session);
|
|
1320
|
+
while (stored.pendingReversionReleases.length > 0) {
|
|
1321
|
+
const release = stored.pendingReversionReleases[0];
|
|
1322
|
+
if (this.reversionProviderProtocolVersion() !== release.protocolVersion) {
|
|
1323
|
+
throw new FlexHarnessValidationError(
|
|
1324
|
+
`Pending workspace release requires protocolVersion ${release.protocolVersion}.`,
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
try {
|
|
1328
|
+
await this.withReversionMaintenanceSignal((signal) =>
|
|
1329
|
+
this.core.turnReversionProvider!.release(Object.freeze({
|
|
1330
|
+
scopeId,
|
|
1331
|
+
scope,
|
|
1332
|
+
storageKey: state.storageKey,
|
|
1333
|
+
sessionId: stored.session.sessionId,
|
|
1334
|
+
...sessionGeneration,
|
|
1335
|
+
runId: release.runId,
|
|
1336
|
+
captureId: release.captureId,
|
|
1337
|
+
reference: cloneSerializable(release.reference),
|
|
1338
|
+
signal,
|
|
1339
|
+
})));
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
if (throwOnFailure) {
|
|
1342
|
+
throw this.core.projectOperationError(
|
|
1343
|
+
error,
|
|
1344
|
+
'turnReversion',
|
|
1345
|
+
scopeId,
|
|
1346
|
+
stored.session.sessionId,
|
|
1347
|
+
`release:${release.captureId}`,
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
try {
|
|
1353
|
+
await this.core.mutateProjection(state, stored, () => {
|
|
1354
|
+
if (stored.pendingReversionReleases[0]?.captureId === release.captureId) {
|
|
1355
|
+
stored.pendingReversionReleases.shift();
|
|
1356
|
+
}
|
|
1357
|
+
}, true);
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
if (!throwOnFailure && state.lifecycle === 'fenced') {
|
|
1360
|
+
this.core.lifecycle.deferReversionReleaseDrain(state, stored, scopeId, scope);
|
|
1361
|
+
}
|
|
1362
|
+
if (throwOnFailure) throw error;
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
if (stored.projectionReconciliationRequired) return;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
public async releaseDeletedSessionReversions(
|
|
1370
|
+
state: IStorageState,
|
|
1371
|
+
storageKey: string,
|
|
1372
|
+
tombstone: IFlexSessionTombstone,
|
|
1373
|
+
scopeId: string,
|
|
1374
|
+
scope: TScope,
|
|
1375
|
+
stored?: IStoredSessionState,
|
|
1376
|
+
): Promise<void> {
|
|
1377
|
+
const sessionId = tombstone.sessionId;
|
|
1378
|
+
const sessionGeneration = requireSessionGeneration(stored?.session ?? tombstone);
|
|
1379
|
+
if (stored) await stored.projectionQueue;
|
|
1380
|
+
let durableProjection = await this.core.stores.projections.load(storageKey, sessionId);
|
|
1381
|
+
if (!durableProjection) return;
|
|
1382
|
+
let projection = this.core.upgradeProjectionSnapshot(durableProjection);
|
|
1383
|
+
const persistDirect = async (next: IFlexProjectionSnapshotCurrent): Promise<void> => {
|
|
1384
|
+
const before = durableProjection;
|
|
1385
|
+
try {
|
|
1386
|
+
await this.core.stores.projections.save(storageKey, sessionId, next, projection.revision);
|
|
1387
|
+
durableProjection = next;
|
|
1388
|
+
projection = next;
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
let current: TFlexProjectionSnapshot | undefined;
|
|
1391
|
+
let reconciliationError: unknown;
|
|
1392
|
+
try {
|
|
1393
|
+
current = await this.core.stores.projections.load(storageKey, sessionId);
|
|
1394
|
+
} catch (loadError) {
|
|
1395
|
+
reconciliationError = loadError;
|
|
1396
|
+
}
|
|
1397
|
+
if (current && JSON.stringify(current) === JSON.stringify(next)) {
|
|
1398
|
+
durableProjection = current;
|
|
1399
|
+
projection = next;
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
if (current && before && JSON.stringify(current) === JSON.stringify(before)) throw error;
|
|
1403
|
+
state.lifecycle = 'fenced';
|
|
1404
|
+
throw reconciliationError === undefined ? error : combineErrors([error, reconciliationError]);
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
if (projection.pendingReversion?.kind === 'apply') {
|
|
1408
|
+
if (stored) {
|
|
1409
|
+
await this.resumePendingApply(state, stored, scopeId, scope);
|
|
1410
|
+
} else {
|
|
1411
|
+
const pending = projection.pendingReversion;
|
|
1412
|
+
const orderedRunIds = pending.direction === 'undo'
|
|
1413
|
+
? [...pending.segmentRunIds].reverse()
|
|
1414
|
+
: [...pending.segmentRunIds];
|
|
1415
|
+
for (const runId of orderedRunIds) {
|
|
1416
|
+
if (pending.appliedRunIds.includes(runId)) continue;
|
|
1417
|
+
const segment = projection.reversionSegments.find((entry) => entry.runId === runId)!;
|
|
1418
|
+
if (segment.disposition === 'revertible') {
|
|
1419
|
+
if (!this.core.turnReversionProvider || !segment.captureId || segment.workspaceReference === undefined) {
|
|
1420
|
+
throw new FlexHarnessValidationError(
|
|
1421
|
+
'Pending workspace reversion deletion requires its turn reversion provider.',
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
if (this.reversionProviderProtocolVersion() !== segment.protocolVersion) {
|
|
1425
|
+
throw new FlexHarnessValidationError(
|
|
1426
|
+
`Pending workspace reversion deletion requires protocolVersion ${segment.protocolVersion}.`,
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
const captureId = segment.captureId;
|
|
1430
|
+
const reference = segment.workspaceReference;
|
|
1431
|
+
const operationId = `${pending.operationId}:${sha256Hex(runId).slice(0, 16)}`;
|
|
1432
|
+
const createContext = (signal: AbortSignal) => Object.freeze({
|
|
1433
|
+
scopeId,
|
|
1434
|
+
scope,
|
|
1435
|
+
storageKey,
|
|
1436
|
+
sessionId,
|
|
1437
|
+
...sessionGeneration,
|
|
1438
|
+
runId,
|
|
1439
|
+
captureId,
|
|
1440
|
+
reference: cloneSerializable(reference),
|
|
1441
|
+
operationId,
|
|
1442
|
+
direction: pending.direction,
|
|
1443
|
+
signal,
|
|
1444
|
+
});
|
|
1445
|
+
let inspection = await this.withReversionMaintenanceSignal((signal) =>
|
|
1446
|
+
this.core.turnReversionProvider!.inspectApply(createContext(signal)));
|
|
1447
|
+
if (inspection.status === 'unknown') {
|
|
1448
|
+
state.lifecycle = 'fenced';
|
|
1449
|
+
throw new FlexHarnessValidationError('Workspace reversion apply outcome is unknown.');
|
|
1450
|
+
}
|
|
1451
|
+
if (inspection.status === 'not-applied') {
|
|
1452
|
+
try {
|
|
1453
|
+
await this.withReversionMaintenanceSignal((signal) =>
|
|
1454
|
+
this.core.turnReversionProvider!.apply(createContext(signal)));
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
inspection = await this.withReversionMaintenanceSignal((signal) =>
|
|
1457
|
+
this.core.turnReversionProvider!.inspectApply(createContext(signal)));
|
|
1458
|
+
if (inspection.status !== 'applied') {
|
|
1459
|
+
if (inspection.status === 'unknown') state.lifecycle = 'fenced';
|
|
1460
|
+
throw error;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
pending.appliedRunIds.push(runId);
|
|
1466
|
+
await persistDirect({ ...projection, revision: projection.revision + 1 });
|
|
1467
|
+
}
|
|
1468
|
+
const { pendingReversion: _pendingReversion, ...completedProjection } = projection;
|
|
1469
|
+
const completed: IFlexProjectionSnapshotCurrent = {
|
|
1470
|
+
...completedProjection,
|
|
1471
|
+
revision: projection.revision + 1,
|
|
1472
|
+
revertCursor: pending.toCursor,
|
|
1473
|
+
};
|
|
1474
|
+
await persistDirect(completed);
|
|
1475
|
+
}
|
|
1476
|
+
const recoveredProjection = await this.core.stores.projections.load(storageKey, sessionId);
|
|
1477
|
+
if (!recoveredProjection) return;
|
|
1478
|
+
durableProjection = recoveredProjection;
|
|
1479
|
+
projection = this.core.upgradeProjectionSnapshot(recoveredProjection);
|
|
1480
|
+
}
|
|
1481
|
+
const releases = new Map<string, IFlexPendingReversionRelease>();
|
|
1482
|
+
for (const release of projection.pendingReversionReleases) {
|
|
1483
|
+
releases.set(release.captureId, cloneSerializable(release));
|
|
1484
|
+
}
|
|
1485
|
+
for (const segment of projection.reversionSegments) {
|
|
1486
|
+
if (segment.captureId && segment.workspaceReference !== undefined) {
|
|
1487
|
+
releases.set(segment.captureId, {
|
|
1488
|
+
runId: segment.runId,
|
|
1489
|
+
captureId: segment.captureId,
|
|
1490
|
+
reference: cloneSerializable(segment.workspaceReference),
|
|
1491
|
+
protocolVersion: segment.protocolVersion,
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
const unresolvedCaptures = new Map<string, {
|
|
1496
|
+
runId: string;
|
|
1497
|
+
captureId: string;
|
|
1498
|
+
protocolVersion: 1 | 2;
|
|
1499
|
+
durableState?: 'preparing' | 'prepared' | 'finalizing';
|
|
1500
|
+
}>();
|
|
1501
|
+
for (const segment of projection.reversionSegments) {
|
|
1502
|
+
if (segment.captureId && !releases.has(segment.captureId)) {
|
|
1503
|
+
unresolvedCaptures.set(segment.captureId, {
|
|
1504
|
+
runId: segment.runId,
|
|
1505
|
+
captureId: segment.captureId,
|
|
1506
|
+
protocolVersion: segment.protocolVersion,
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
if (projection.pendingReversion?.kind === 'capture') {
|
|
1511
|
+
unresolvedCaptures.set(projection.pendingReversion.captureId, {
|
|
1512
|
+
runId: projection.pendingReversion.runId,
|
|
1513
|
+
captureId: projection.pendingReversion.captureId,
|
|
1514
|
+
protocolVersion: projection.pendingReversion.protocolVersion,
|
|
1515
|
+
durableState: projection.pendingReversion.state,
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
if (unresolvedCaptures.size > 0 && !this.core.turnReversionProvider) {
|
|
1519
|
+
throw new FlexHarnessValidationError(
|
|
1520
|
+
'Capture-backed session deletion requires its turn reversion provider.',
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
for (const capture of unresolvedCaptures.values()) {
|
|
1524
|
+
if (this.reversionProviderProtocolVersion() !== capture.protocolVersion) {
|
|
1525
|
+
throw new FlexHarnessValidationError(
|
|
1526
|
+
`Deleted session capture requires protocolVersion ${capture.protocolVersion}.`,
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
if (capture.durableState === 'prepared') {
|
|
1530
|
+
if (stored) {
|
|
1531
|
+
await this.core.mutateProjection(state, stored, () => {
|
|
1532
|
+
const pending = stored.pendingReversion;
|
|
1533
|
+
if (pending?.kind !== 'capture' || pending.captureId !== capture.captureId) {
|
|
1534
|
+
throw new FlexHarnessValidationError('Deleted session capture changed before finalization.');
|
|
1535
|
+
}
|
|
1536
|
+
pending.state = 'finalizing';
|
|
1537
|
+
}, true);
|
|
1538
|
+
} else if (projection.pendingReversion?.kind === 'capture') {
|
|
1539
|
+
projection.pendingReversion.state = 'finalizing';
|
|
1540
|
+
await persistDirect({ ...projection, revision: projection.revision + 1 });
|
|
1541
|
+
}
|
|
1542
|
+
capture.durableState = 'finalizing';
|
|
1543
|
+
}
|
|
1544
|
+
let inspection;
|
|
1545
|
+
try {
|
|
1546
|
+
inspection = await this.inspectReversionCapture(
|
|
1547
|
+
scopeId,
|
|
1548
|
+
scope,
|
|
1549
|
+
storageKey,
|
|
1550
|
+
sessionId,
|
|
1551
|
+
sessionGeneration,
|
|
1552
|
+
capture.runId,
|
|
1553
|
+
capture.captureId,
|
|
1554
|
+
);
|
|
1555
|
+
} catch (error) {
|
|
1556
|
+
throw this.core.projectOperationError(
|
|
1557
|
+
error,
|
|
1558
|
+
'turnReversion',
|
|
1559
|
+
scopeId,
|
|
1560
|
+
sessionId,
|
|
1561
|
+
`release:${capture.captureId}`,
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
if (inspection.status === 'unknown') {
|
|
1565
|
+
state.lifecycle = 'fenced';
|
|
1566
|
+
throw new FlexHarnessValidationError('Deleted session capture outcome is unknown.');
|
|
1567
|
+
}
|
|
1568
|
+
if (inspection.status === 'missing') {
|
|
1569
|
+
if (capture.durableState === 'preparing') continue;
|
|
1570
|
+
state.lifecycle = 'fenced';
|
|
1571
|
+
throw new FlexHarnessValidationError('Prepared deleted-session capture is missing.');
|
|
1572
|
+
}
|
|
1573
|
+
let outcome = inspection.status === 'finalized' ? inspection.outcome : undefined;
|
|
1574
|
+
if (inspection.status === 'prepared') {
|
|
1575
|
+
try {
|
|
1576
|
+
outcome = await this.finalizeReversionCapture(
|
|
1577
|
+
scopeId,
|
|
1578
|
+
scope,
|
|
1579
|
+
storageKey,
|
|
1580
|
+
sessionId,
|
|
1581
|
+
sessionGeneration,
|
|
1582
|
+
capture.runId,
|
|
1583
|
+
capture.captureId,
|
|
1584
|
+
);
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
const finalInspection = await this.inspectReversionCapture(
|
|
1587
|
+
scopeId,
|
|
1588
|
+
scope,
|
|
1589
|
+
storageKey,
|
|
1590
|
+
sessionId,
|
|
1591
|
+
sessionGeneration,
|
|
1592
|
+
capture.runId,
|
|
1593
|
+
capture.captureId,
|
|
1594
|
+
);
|
|
1595
|
+
if (finalInspection.status !== 'finalized') {
|
|
1596
|
+
if (finalInspection.status === 'unknown') state.lifecycle = 'fenced';
|
|
1597
|
+
throw this.core.projectOperationError(
|
|
1598
|
+
error,
|
|
1599
|
+
'turnReversion',
|
|
1600
|
+
scopeId,
|
|
1601
|
+
sessionId,
|
|
1602
|
+
`release:${capture.captureId}`,
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
outcome = finalInspection.outcome;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
if (outcome === undefined) {
|
|
1609
|
+
throw new FlexHarnessValidationError('Deleted session capture has no release reference.');
|
|
1610
|
+
}
|
|
1611
|
+
releases.set(capture.captureId, {
|
|
1612
|
+
runId: capture.runId,
|
|
1613
|
+
captureId: capture.captureId,
|
|
1614
|
+
reference: cloneSerializable(outcome.reference),
|
|
1615
|
+
protocolVersion: capture.protocolVersion,
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
if (releases.size === 0) return;
|
|
1619
|
+
if (!this.core.turnReversionProvider) {
|
|
1620
|
+
throw new FlexHarnessValidationError(
|
|
1621
|
+
'Capture-backed session deletion requires its turn reversion provider.',
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
if (stored) {
|
|
1625
|
+
await this.core.mutateProjection(state, stored, () => {
|
|
1626
|
+
stored.reversionSegments = [];
|
|
1627
|
+
stored.revertCursor = 0;
|
|
1628
|
+
delete stored.pendingReversion;
|
|
1629
|
+
stored.pendingReversionReleases = [...releases.values()].map((release) =>
|
|
1630
|
+
cloneSerializable(release));
|
|
1631
|
+
}, true);
|
|
1632
|
+
await this.drainReversionReleases(state, stored, scopeId, scope, true);
|
|
1633
|
+
} else {
|
|
1634
|
+
const { pendingReversion: _pendingReversion, ...withoutPending } = projection;
|
|
1635
|
+
await persistDirect({
|
|
1636
|
+
...withoutPending,
|
|
1637
|
+
revision: projection.revision + 1,
|
|
1638
|
+
reversionSegments: [],
|
|
1639
|
+
revertCursor: 0,
|
|
1640
|
+
pendingReversionReleases: [...releases.values()].map((release) =>
|
|
1641
|
+
cloneSerializable(release)),
|
|
1642
|
+
});
|
|
1643
|
+
while (projection.pendingReversionReleases.length > 0) {
|
|
1644
|
+
const release = projection.pendingReversionReleases[0];
|
|
1645
|
+
if (this.reversionProviderProtocolVersion() !== release.protocolVersion) {
|
|
1646
|
+
throw new FlexHarnessValidationError(
|
|
1647
|
+
`Deleted session release requires protocolVersion ${release.protocolVersion}.`,
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
try {
|
|
1651
|
+
await this.withReversionMaintenanceSignal((signal) =>
|
|
1652
|
+
this.core.turnReversionProvider!.release(Object.freeze({
|
|
1653
|
+
scopeId,
|
|
1654
|
+
scope,
|
|
1655
|
+
storageKey,
|
|
1656
|
+
sessionId,
|
|
1657
|
+
...sessionGeneration,
|
|
1658
|
+
runId: release.runId,
|
|
1659
|
+
captureId: release.captureId,
|
|
1660
|
+
reference: cloneSerializable(release.reference),
|
|
1661
|
+
signal,
|
|
1662
|
+
})));
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
throw this.core.projectOperationError(
|
|
1665
|
+
error,
|
|
1666
|
+
'turnReversion',
|
|
1667
|
+
scopeId,
|
|
1668
|
+
sessionId,
|
|
1669
|
+
`release:${release.captureId}`,
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
await persistDirect({
|
|
1673
|
+
...projection,
|
|
1674
|
+
revision: projection.revision + 1,
|
|
1675
|
+
pendingReversionReleases: projection.pendingReversionReleases.slice(1),
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
public completedReversionCandidates(stored: IStoredSessionState): IFlexReversionSegment[] {
|
|
1682
|
+
return stored.reversionSegments.filter((segment) => segment.status === 'completed');
|
|
1683
|
+
}
|
|
1684
|
+
}
|