@modelprofile.com/flexharness 3.7.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +9 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +14 -2
- package/dist_ts/classes.flexharness.js +667 -199
- package/dist_ts/interfaces.d.ts +90 -6
- package/dist_ts/interfaces.js +5 -1
- package/dist_ts/utils.json.d.ts +3 -2
- package/dist_ts/utils.json.js +243 -11
- package/dist_ts_migration/v3_legacyflexharness.js +1 -1
- package/package.json +1 -1
- package/readme.hints.md +3 -2
- package/readme.md +86 -10
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +757 -198
- package/ts/classes.stores.ts +3 -3
- package/ts/interfaces.ts +125 -6
- package/ts/utils.json.ts +271 -12
- package/ts_migration/v3_legacyflexharness.ts +1 -1
|
@@ -18,13 +18,18 @@ import {
|
|
|
18
18
|
errorToInfo,
|
|
19
19
|
} from './errors.js';
|
|
20
20
|
import {
|
|
21
|
+
FLEX_REVERSION_MAX_AFFECTED_WORKSPACES,
|
|
21
22
|
FLEX_REVERSION_DEFAULT_LIMITS,
|
|
22
23
|
FLEX_REVERSION_MAXIMUM_LIMITS,
|
|
24
|
+
FLEX_REVERSION_REASON_CODE_MAX_BYTES,
|
|
23
25
|
FLEX_REVERSION_REFERENCE_MAX_BYTES,
|
|
26
|
+
FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES,
|
|
27
|
+
FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES,
|
|
24
28
|
} from './interfaces.js';
|
|
25
29
|
import type {
|
|
26
30
|
IFlexAgentContextInvocation,
|
|
27
31
|
IFlexAgentSessionPolicy,
|
|
32
|
+
IFlexAffectedWorkspace,
|
|
28
33
|
IFlexAttachmentMessagePart,
|
|
29
34
|
IFlexBackgroundExecution,
|
|
30
35
|
IFlexCallbackLimits,
|
|
@@ -61,6 +66,8 @@ import type {
|
|
|
61
66
|
IFlexSchedulePromptOptions,
|
|
62
67
|
IFlexScopeSnapshot,
|
|
63
68
|
IFlexSession,
|
|
69
|
+
IFlexSessionReversionGroup,
|
|
70
|
+
IFlexSessionReversionInfo,
|
|
64
71
|
IFlexSessionTombstone,
|
|
65
72
|
IFlexSlashCommandDescriptor,
|
|
66
73
|
IFlexSlashCommandExecutionOptions,
|
|
@@ -93,6 +100,9 @@ import type {
|
|
|
93
100
|
TFlexSlashCommandRegistration,
|
|
94
101
|
TFlexPendingReversion,
|
|
95
102
|
TFlexProjectionSnapshot,
|
|
103
|
+
TFlexReversionPolicy,
|
|
104
|
+
TFlexSessionReversionGroupKind,
|
|
105
|
+
TFlexTurnReversionFinalizedOutcome,
|
|
96
106
|
TFlexToolExecutionReconciliation,
|
|
97
107
|
TJsonValue,
|
|
98
108
|
} from './interfaces.js';
|
|
@@ -138,7 +148,7 @@ interface IStoredSessionState {
|
|
|
138
148
|
excludedRunIds: string[];
|
|
139
149
|
pendingReversion?: TFlexPendingReversion;
|
|
140
150
|
pendingReversionReleases: IFlexPendingReversionRelease[];
|
|
141
|
-
projectionSchemaVersion: 1 | 2;
|
|
151
|
+
projectionSchemaVersion: 1 | 2 | 3;
|
|
142
152
|
projectionBaseline?: TFlexProjectionSnapshot;
|
|
143
153
|
projectionReconciliationRequired: boolean;
|
|
144
154
|
projectionRevision: number;
|
|
@@ -168,6 +178,18 @@ interface IRetainedSessionCleanup {
|
|
|
168
178
|
domainsCompleted: boolean;
|
|
169
179
|
}
|
|
170
180
|
|
|
181
|
+
interface IReversionGroup {
|
|
182
|
+
target: IFlexReversionSegment;
|
|
183
|
+
segments: IFlexReversionSegment[];
|
|
184
|
+
kind: TFlexSessionReversionGroupKind;
|
|
185
|
+
affectedWorkspaces: IFlexAffectedWorkspace[];
|
|
186
|
+
affectedWorkspacesTruncated: boolean;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
type TNormalizedCaptureInspection =
|
|
190
|
+
| { status: 'missing' | 'prepared' | 'unknown' }
|
|
191
|
+
| { status: 'finalized'; outcome: TFlexTurnReversionFinalizedOutcome };
|
|
192
|
+
|
|
171
193
|
interface IDetachedCleanup {
|
|
172
194
|
cleanup: () => Promise<void> | void;
|
|
173
195
|
projectError: (error: unknown) => Error;
|
|
@@ -332,6 +354,12 @@ interface ISlashCommandInvocationOwner {
|
|
|
332
354
|
sessionKey: string;
|
|
333
355
|
}
|
|
334
356
|
|
|
357
|
+
interface ISlashCommandActivityOwner {
|
|
358
|
+
scopeId: string;
|
|
359
|
+
storageKey: string;
|
|
360
|
+
sessionId: string;
|
|
361
|
+
}
|
|
362
|
+
|
|
335
363
|
interface IRunResultProjection {
|
|
336
364
|
text: string;
|
|
337
365
|
steps: number;
|
|
@@ -1113,6 +1141,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1113
1141
|
private readonly promptQueueLimits: Required<IFlexPromptQueueLimits>;
|
|
1114
1142
|
private readonly reversionLimits: Required<IFlexReversionLimits>;
|
|
1115
1143
|
private readonly turnReversionProvider: IFlexHarnessOptions<TScope>['turnReversionProvider'];
|
|
1144
|
+
private readonly reversionPolicy: TFlexReversionPolicy;
|
|
1116
1145
|
private readonly externalErrorProjector?: TFlexExternalErrorProjector;
|
|
1117
1146
|
private readonly subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
|
|
1118
1147
|
private readonly slashCommands: ReadonlyMap<string, TRegisteredSlashCommand<TScope>>;
|
|
@@ -1143,6 +1172,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1143
1172
|
private readonly slashCommandInvocationContext = new plugins.AsyncLocalStorage<
|
|
1144
1173
|
ISlashCommandInvocationOwner
|
|
1145
1174
|
>();
|
|
1175
|
+
private readonly slashCommandActivityContext = new plugins.AsyncLocalStorage<
|
|
1176
|
+
ISlashCommandActivityOwner
|
|
1177
|
+
>();
|
|
1146
1178
|
private readonly deferredCompactorContexts = new WeakMap<
|
|
1147
1179
|
IFlexAgentContextInvocation<unknown>,
|
|
1148
1180
|
IFlexAgentContextInvocation<unknown>
|
|
@@ -1170,6 +1202,24 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1170
1202
|
this.promptQueueLimits = resolvePromptQueueLimits(options.promptQueueLimits);
|
|
1171
1203
|
this.reversionLimits = resolveReversionLimits(options.reversionLimits);
|
|
1172
1204
|
this.turnReversionProvider = options.turnReversionProvider;
|
|
1205
|
+
this.reversionPolicy = options.reversionPolicy ?? 'transcript-optional';
|
|
1206
|
+
if (!['transcript-optional', 'workspace-required'].includes(this.reversionPolicy)) {
|
|
1207
|
+
throw new FlexHarnessValidationError(
|
|
1208
|
+
'reversionPolicy must be "transcript-optional" or "workspace-required".',
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
const protocolVersion = this.turnReversionProvider
|
|
1212
|
+
&& 'protocolVersion' in this.turnReversionProvider
|
|
1213
|
+
? this.turnReversionProvider.protocolVersion
|
|
1214
|
+
: 1;
|
|
1215
|
+
if (protocolVersion !== 1 && protocolVersion !== 2) {
|
|
1216
|
+
throw new FlexHarnessValidationError('Turn reversion provider protocolVersion is invalid.');
|
|
1217
|
+
}
|
|
1218
|
+
if (this.reversionPolicy === 'workspace-required' && protocolVersion !== 2) {
|
|
1219
|
+
throw new FlexHarnessValidationError(
|
|
1220
|
+
'workspace-required reversion policy requires a protocolVersion 2 turn reversion provider.',
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1173
1223
|
this.externalErrorProjector = options.externalErrorProjector;
|
|
1174
1224
|
this.subagents = normalizeSubagents(options.subagents);
|
|
1175
1225
|
this.slashCommands = normalizeSlashCommands(options.slashCommands);
|
|
@@ -1357,6 +1407,41 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1357
1407
|
return publicSnapshot(this.requireSession(state, sessionId).session);
|
|
1358
1408
|
}
|
|
1359
1409
|
|
|
1410
|
+
public async getSessionReversionInfo(
|
|
1411
|
+
scopeId: string,
|
|
1412
|
+
sessionId: string,
|
|
1413
|
+
): Promise<IFlexSessionReversionInfo> {
|
|
1414
|
+
const { state } = await this.resolveState(scopeId);
|
|
1415
|
+
const stored = this.requireSession(state, sessionId);
|
|
1416
|
+
await stored.projectionQueue;
|
|
1417
|
+
this.assertStateAcceptingWork(state);
|
|
1418
|
+
await this.reconcileContextAvailability(state, stored);
|
|
1419
|
+
const unavailableReason = await this.reversionUnavailableReason(state, stored);
|
|
1420
|
+
const groups = this.reversionGroups(stored);
|
|
1421
|
+
const undo = this.reversionUnit(stored, 'undo');
|
|
1422
|
+
const redo = this.reversionUnit(stored, 'redo');
|
|
1423
|
+
const isAvailable = (unit: ReturnType<typeof this.reversionUnit>) => Boolean(
|
|
1424
|
+
!unavailableReason
|
|
1425
|
+
&& unit
|
|
1426
|
+
&& unit.target.contextAvailable
|
|
1427
|
+
&& !unit.segments.some((segment) =>
|
|
1428
|
+
segment.provenance === 'workspace'
|
|
1429
|
+
&& segment.disposition === 'revertible'
|
|
1430
|
+
&& segment.workspaceReference === undefined),
|
|
1431
|
+
);
|
|
1432
|
+
return publicSnapshot({
|
|
1433
|
+
undoAvailable: isAvailable(undo),
|
|
1434
|
+
redoAvailable: isAvailable(redo),
|
|
1435
|
+
groups: groups.map((group, index): IFlexSessionReversionGroup => ({
|
|
1436
|
+
runId: group.target.runId,
|
|
1437
|
+
kind: group.kind,
|
|
1438
|
+
visibility: index < stored.revertCursor ? 'visible' : 'hidden',
|
|
1439
|
+
affectedWorkspaces: cloneSerializable(group.affectedWorkspaces),
|
|
1440
|
+
affectedWorkspacesTruncated: group.affectedWorkspacesTruncated,
|
|
1441
|
+
})),
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1360
1445
|
public async updateSession(
|
|
1361
1446
|
scopeId: string,
|
|
1362
1447
|
sessionId: string,
|
|
@@ -1384,14 +1469,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1384
1469
|
}
|
|
1385
1470
|
|
|
1386
1471
|
public async deleteSession(scopeId: string, sessionId: string): Promise<void> {
|
|
1387
|
-
|
|
1388
|
-
if (invocationOwner?.scopeId === scopeId && invocationOwner.sessionId === sessionId) {
|
|
1389
|
-
throw this.trustInternalError(new FlexHarnessSlashCommandReentryError(sessionId));
|
|
1390
|
-
}
|
|
1472
|
+
this.assertNoAmbientSlashCommandTeardown({ scopeId, sessionId });
|
|
1391
1473
|
const { scope, state } = await this.resolveState(scopeId);
|
|
1392
|
-
|
|
1393
|
-
throw this.trustInternalError(new FlexHarnessSlashCommandReentryError(invocationOwner.sessionId));
|
|
1394
|
-
}
|
|
1474
|
+
this.assertNoAmbientSlashCommandTeardown({ storageKey: state.storageKey });
|
|
1395
1475
|
validateIdentifier(sessionId, 'sessionId');
|
|
1396
1476
|
await state.scopeQueue;
|
|
1397
1477
|
const deletionKey = state.tombstones.get(sessionId)?.rootSessionId ?? sessionId;
|
|
@@ -1606,7 +1686,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1606
1686
|
sessionId: string,
|
|
1607
1687
|
): Promise<IFlexSlashCommandDescriptor[]> {
|
|
1608
1688
|
const { state } = await this.resolveState(scopeId);
|
|
1609
|
-
return this.trackSlashCommandListing(state, sessionId, async (signal) => {
|
|
1689
|
+
return this.trackSlashCommandListing(scopeId, state, sessionId, async (signal) => {
|
|
1610
1690
|
await this.awaitReversionOperation(state.scopeQueue, signal);
|
|
1611
1691
|
signal.throwIfAborted();
|
|
1612
1692
|
this.assertStateAcceptingWork(state);
|
|
@@ -1644,7 +1724,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1644
1724
|
const workspaceReversion = (unit: ReturnType<typeof this.reversionUnit>) =>
|
|
1645
1725
|
!unit
|
|
1646
1726
|
|| !this.turnReversionProvider
|
|
1647
|
-
|| unit.segments.some((segment) =>
|
|
1727
|
+
|| unit.segments.some((segment) => segment.provenance !== 'workspace')
|
|
1648
1728
|
? 'unsupported' as const
|
|
1649
1729
|
: 'supported' as const;
|
|
1650
1730
|
const reversionAvailability = (unit: ReturnType<typeof this.reversionUnit>) => {
|
|
@@ -1658,7 +1738,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1658
1738
|
unavailableReason: 'The turn is beyond the context archive horizon.',
|
|
1659
1739
|
};
|
|
1660
1740
|
if (unit.segments.some((segment) =>
|
|
1661
|
-
segment.
|
|
1741
|
+
segment.disposition === 'revertible' && segment.workspaceReference === undefined)) return {
|
|
1662
1742
|
available: false,
|
|
1663
1743
|
unavailableReason: 'The turn has no available workspace reversion capture.',
|
|
1664
1744
|
};
|
|
@@ -1802,6 +1882,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1802
1882
|
}
|
|
1803
1883
|
this.requireSessionAvailableForSlashCommand(state, stored, parsed.name);
|
|
1804
1884
|
return this.trackSlashCommandExecution(
|
|
1885
|
+
scopeId,
|
|
1805
1886
|
state,
|
|
1806
1887
|
sessionId,
|
|
1807
1888
|
'operation',
|
|
@@ -1829,6 +1910,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1829
1910
|
this.promptAdmissionByteSize(expanded, promptOptions),
|
|
1830
1911
|
);
|
|
1831
1912
|
return this.trackSlashCommandExecution(
|
|
1913
|
+
scopeId,
|
|
1832
1914
|
state,
|
|
1833
1915
|
sessionId,
|
|
1834
1916
|
'prompt-admission',
|
|
@@ -2001,6 +2083,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2001
2083
|
}
|
|
2002
2084
|
|
|
2003
2085
|
private trackSlashCommandExecution<TResult>(
|
|
2086
|
+
scopeId: string,
|
|
2004
2087
|
state: IStorageState,
|
|
2005
2088
|
sessionId: string,
|
|
2006
2089
|
kind: IActiveSlashCommandExecution['kind'],
|
|
@@ -2024,10 +2107,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2024
2107
|
};
|
|
2025
2108
|
externalSignal?.addEventListener('abort', abortFromExternalSignal, { once: true });
|
|
2026
2109
|
if (externalSignal?.aborted) abortFromExternalSignal();
|
|
2027
|
-
const
|
|
2110
|
+
const activityOwner = Object.freeze({ scopeId, storageKey: state.storageKey, sessionId });
|
|
2111
|
+
const completion = Promise.resolve().then(() => this.slashCommandActivityContext.run(activityOwner, () => {
|
|
2028
2112
|
if (controller.signal.aborted) throw controller.signal.reason;
|
|
2029
2113
|
return operation(controller.signal);
|
|
2030
|
-
});
|
|
2114
|
+
}));
|
|
2031
2115
|
const active: IActiveSlashCommandExecution = {
|
|
2032
2116
|
kind,
|
|
2033
2117
|
storageKey: state.storageKey,
|
|
@@ -2045,12 +2129,15 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2045
2129
|
}
|
|
2046
2130
|
|
|
2047
2131
|
private trackSlashCommandListing<TResult>(
|
|
2132
|
+
scopeId: string,
|
|
2048
2133
|
state: IStorageState,
|
|
2049
2134
|
sessionId: string,
|
|
2050
2135
|
operation: (signal: AbortSignal) => Promise<TResult>,
|
|
2051
2136
|
): Promise<TResult> {
|
|
2052
2137
|
const controller = new AbortController();
|
|
2053
|
-
const
|
|
2138
|
+
const activityOwner = Object.freeze({ scopeId, storageKey: state.storageKey, sessionId });
|
|
2139
|
+
const completion = Promise.resolve().then(() =>
|
|
2140
|
+
this.slashCommandActivityContext.run(activityOwner, () => operation(controller.signal)));
|
|
2054
2141
|
const active: IActiveSlashCommandListing = {
|
|
2055
2142
|
storageKey: state.storageKey,
|
|
2056
2143
|
sessionId,
|
|
@@ -2094,6 +2181,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2094
2181
|
sessionKey,
|
|
2095
2182
|
});
|
|
2096
2183
|
return this.trackSlashCommandExecution(
|
|
2184
|
+
scopeId,
|
|
2097
2185
|
state,
|
|
2098
2186
|
stored.session.sessionId,
|
|
2099
2187
|
'handler',
|
|
@@ -2403,6 +2491,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2403
2491
|
}
|
|
2404
2492
|
this.requireSessionAvailableForSlashCommand(state, stored, 'compact');
|
|
2405
2493
|
await this.trackSlashCommandExecution(
|
|
2494
|
+
scopeId,
|
|
2406
2495
|
state,
|
|
2407
2496
|
sessionId,
|
|
2408
2497
|
'operation',
|
|
@@ -2464,24 +2553,77 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2464
2553
|
stored: IStoredSessionState,
|
|
2465
2554
|
direction: 'undo' | 'redo',
|
|
2466
2555
|
): { target: IFlexReversionSegment; segments: IFlexReversionSegment[]; toCursor: number } | undefined {
|
|
2467
|
-
const
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2556
|
+
const groups = this.reversionGroups(stored);
|
|
2557
|
+
if (direction === 'undo') {
|
|
2558
|
+
let start = stored.revertCursor - 1;
|
|
2559
|
+
while (start >= 0 && groups[start]?.kind === 'no-change') start--;
|
|
2560
|
+
const candidate = groups[start];
|
|
2561
|
+
if (!candidate || candidate.kind !== 'candidate') return undefined;
|
|
2562
|
+
return {
|
|
2563
|
+
target: candidate.target,
|
|
2564
|
+
segments: groups.slice(start, stored.revertCursor).flatMap((group) => group.segments),
|
|
2565
|
+
toCursor: start,
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
const candidate = groups[stored.revertCursor];
|
|
2569
|
+
if (!candidate || candidate.kind !== 'candidate') return undefined;
|
|
2570
|
+
let end = stored.revertCursor + 1;
|
|
2571
|
+
while (groups[end]?.kind === 'no-change') end++;
|
|
2478
2572
|
return {
|
|
2479
|
-
target: candidate,
|
|
2480
|
-
segments:
|
|
2481
|
-
toCursor:
|
|
2573
|
+
target: candidate.target,
|
|
2574
|
+
segments: groups.slice(stored.revertCursor, end).flatMap((group) => group.segments),
|
|
2575
|
+
toCursor: end,
|
|
2482
2576
|
};
|
|
2483
2577
|
}
|
|
2484
2578
|
|
|
2579
|
+
private reversionGroups(stored: IStoredSessionState): IReversionGroup[] {
|
|
2580
|
+
const groups: IReversionGroup[] = [];
|
|
2581
|
+
let leading: IFlexReversionSegment[] = [];
|
|
2582
|
+
for (const segment of stored.reversionSegments) {
|
|
2583
|
+
if (segment.status === 'completed') {
|
|
2584
|
+
const segments = groups.length === 0 ? [...leading, segment] : [segment];
|
|
2585
|
+
leading = [];
|
|
2586
|
+
groups.push({
|
|
2587
|
+
target: segment,
|
|
2588
|
+
segments,
|
|
2589
|
+
kind: 'candidate',
|
|
2590
|
+
affectedWorkspaces: [],
|
|
2591
|
+
affectedWorkspacesTruncated: false,
|
|
2592
|
+
});
|
|
2593
|
+
} else if (groups.length > 0) {
|
|
2594
|
+
groups[groups.length - 1].segments.push(segment);
|
|
2595
|
+
} else {
|
|
2596
|
+
leading.push(segment);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
for (const group of groups) {
|
|
2600
|
+
const affected = new Map<string, IFlexAffectedWorkspace>();
|
|
2601
|
+
for (const workspace of group.segments.flatMap((segment) =>
|
|
2602
|
+
segment.affectedWorkspaces ?? [])) {
|
|
2603
|
+
if (affected.has(workspace.id)) continue;
|
|
2604
|
+
if (affected.size < FLEX_REVERSION_MAX_AFFECTED_WORKSPACES) {
|
|
2605
|
+
affected.set(workspace.id, cloneSerializable(workspace));
|
|
2606
|
+
} else {
|
|
2607
|
+
group.affectedWorkspacesTruncated = true;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
group.affectedWorkspaces = [...affected.values()];
|
|
2611
|
+
if (this.reversionPolicy === 'transcript-optional') continue;
|
|
2612
|
+
if (group.segments.some((segment) =>
|
|
2613
|
+
segment.status === 'capturing'
|
|
2614
|
+
|| segment.disposition === 'pending'
|
|
2615
|
+
|| (segment.protocolVersion === 1 && segment.provenance === 'transcript')
|
|
2616
|
+
|| segment.disposition === 'nonrevertible')) {
|
|
2617
|
+
group.kind = 'barrier';
|
|
2618
|
+
} else if (group.segments.some((segment) => segment.disposition === 'revertible')) {
|
|
2619
|
+
group.kind = 'candidate';
|
|
2620
|
+
} else {
|
|
2621
|
+
group.kind = 'no-change';
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
return groups;
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2485
2627
|
private async requireReversionIdle(
|
|
2486
2628
|
state: IStorageState,
|
|
2487
2629
|
stored: IStoredSessionState,
|
|
@@ -2587,6 +2729,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2587
2729
|
throw this.trustInternalError(new FlexHarnessSlashCommandReentryError(sessionId));
|
|
2588
2730
|
}
|
|
2589
2731
|
return this.trackSlashCommandExecution(
|
|
2732
|
+
scopeId,
|
|
2590
2733
|
state,
|
|
2591
2734
|
sessionId,
|
|
2592
2735
|
'operation',
|
|
@@ -2609,8 +2752,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2609
2752
|
if (pending.direction !== direction) {
|
|
2610
2753
|
throw new FlexHarnessSessionBusyError(sessionId, 'has a pending opposite reversion operation');
|
|
2611
2754
|
}
|
|
2612
|
-
const
|
|
2613
|
-
|
|
2755
|
+
const target = pending.segmentRunIds
|
|
2756
|
+
.map((runId) => stored.reversionSegments.find((segment) => segment.runId === runId))
|
|
2757
|
+
.find((segment) => segment?.status === 'completed');
|
|
2614
2758
|
if (!target) throw new FlexHarnessValidationError('Pending reversion has no target candidate.');
|
|
2615
2759
|
await this.resumePendingApply(state, stored, scopeId, scope, operationSignal);
|
|
2616
2760
|
this.emitEvent(scopeId, sessionId, {
|
|
@@ -2634,7 +2778,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2634
2778
|
);
|
|
2635
2779
|
}
|
|
2636
2780
|
if (this.turnReversionProvider && unit.segments.some((segment) =>
|
|
2637
|
-
segment.
|
|
2781
|
+
segment.disposition === 'revertible' && segment.workspaceReference === undefined)) {
|
|
2638
2782
|
throw new FlexHarnessSlashCommandUnavailableError(
|
|
2639
2783
|
direction,
|
|
2640
2784
|
'The turn has no available workspace reversion capture.',
|
|
@@ -2713,7 +2857,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2713
2857
|
if (pending?.kind !== 'apply') return;
|
|
2714
2858
|
const segmentMap = new Map(stored.reversionSegments.map((segment) => [segment.runId, segment]));
|
|
2715
2859
|
if (
|
|
2716
|
-
pending.segmentRunIds.some((runId) =>
|
|
2860
|
+
pending.segmentRunIds.some((runId) =>
|
|
2861
|
+
segmentMap.get(runId)?.disposition === 'revertible')
|
|
2717
2862
|
&& !this.turnReversionProvider
|
|
2718
2863
|
) {
|
|
2719
2864
|
throw new FlexHarnessValidationError(
|
|
@@ -2723,18 +2868,25 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2723
2868
|
const orderedRunIds = pending.direction === 'undo'
|
|
2724
2869
|
? [...pending.segmentRunIds].reverse()
|
|
2725
2870
|
: [...pending.segmentRunIds];
|
|
2871
|
+
let workspaceProgress = pending.appliedRunIds.some((runId) =>
|
|
2872
|
+
segmentMap.get(runId)?.disposition === 'revertible');
|
|
2726
2873
|
for (const runId of orderedRunIds) {
|
|
2727
2874
|
if (pending.appliedRunIds.includes(runId)) continue;
|
|
2728
|
-
if (signal?.aborted) {
|
|
2875
|
+
if (signal?.aborted && !workspaceProgress) {
|
|
2729
2876
|
await this.persistKnownNotApplied(state, stored, pending.operationId, runId);
|
|
2730
2877
|
throw signal.reason;
|
|
2731
2878
|
}
|
|
2732
2879
|
const segment = segmentMap.get(runId);
|
|
2733
2880
|
if (!segment) throw new FlexHarnessValidationError('Pending reversion references a missing segment.');
|
|
2734
|
-
if (segment.
|
|
2881
|
+
if (segment.disposition === 'revertible') {
|
|
2735
2882
|
if (!this.turnReversionProvider || !segment.captureId || segment.workspaceReference === undefined) {
|
|
2736
2883
|
throw new FlexHarnessValidationError('Pending workspace reversion segment is incomplete.');
|
|
2737
2884
|
}
|
|
2885
|
+
if (this.reversionProviderProtocolVersion() !== segment.protocolVersion) {
|
|
2886
|
+
throw new FlexHarnessValidationError(
|
|
2887
|
+
`Pending workspace reversion requires protocolVersion ${segment.protocolVersion}.`,
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2738
2890
|
const captureId = segment.captureId;
|
|
2739
2891
|
const reference = segment.workspaceReference;
|
|
2740
2892
|
const childOperationId = `${pending.operationId}:${sha256Hex(runId).slice(0, 16)}`;
|
|
@@ -2770,7 +2922,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2770
2922
|
}
|
|
2771
2923
|
if (inspection.status === 'not-applied') {
|
|
2772
2924
|
try {
|
|
2773
|
-
if (signal) {
|
|
2925
|
+
if (signal && !workspaceProgress) {
|
|
2774
2926
|
await this.turnReversionProvider.apply(createContext(signal));
|
|
2775
2927
|
} else {
|
|
2776
2928
|
await this.withReversionMaintenanceSignal((maintenanceSignal) =>
|
|
@@ -2811,6 +2963,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2811
2963
|
}
|
|
2812
2964
|
if (!current.appliedRunIds.includes(runId)) current.appliedRunIds.push(runId);
|
|
2813
2965
|
}, true);
|
|
2966
|
+
if (segment.disposition === 'revertible') workspaceProgress = true;
|
|
2814
2967
|
}
|
|
2815
2968
|
await this.mutateProjection(state, stored, () => {
|
|
2816
2969
|
const current = stored.pendingReversion;
|
|
@@ -2834,7 +2987,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2834
2987
|
throw new FlexHarnessValidationError('Pending reversion changed while recording failed apply.');
|
|
2835
2988
|
}
|
|
2836
2989
|
current.appliedRunIds = current.appliedRunIds.filter((entry) => entry !== runId);
|
|
2837
|
-
|
|
2990
|
+
const hasWorkspaceProgress = current.appliedRunIds.some((appliedRunId) =>
|
|
2991
|
+
stored.reversionSegments.some((segment) =>
|
|
2992
|
+
segment.runId === appliedRunId && segment.disposition === 'revertible'));
|
|
2993
|
+
if (!hasWorkspaceProgress) {
|
|
2838
2994
|
stored.revertCursor = current.fromCursor;
|
|
2839
2995
|
delete stored.pendingReversion;
|
|
2840
2996
|
}
|
|
@@ -2852,17 +3008,31 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2852
3008
|
if (pending?.kind !== 'capture' || !this.turnReversionProvider) return;
|
|
2853
3009
|
const segment = stored.reversionSegments.find((entry) => entry.runId === pending.runId);
|
|
2854
3010
|
if (!segment) throw new FlexHarnessValidationError('Pending capture references a missing segment.');
|
|
3011
|
+
if (this.reversionProviderProtocolVersion() !== pending.protocolVersion) {
|
|
3012
|
+
throw new FlexHarnessValidationError(
|
|
3013
|
+
`Pending capture recovery requires protocolVersion ${pending.protocolVersion}.`,
|
|
3014
|
+
);
|
|
3015
|
+
}
|
|
2855
3016
|
const outcome = outcomes.get(pending.runId);
|
|
2856
|
-
|
|
3017
|
+
if (pending.state === 'prepared') {
|
|
3018
|
+
await this.mutateProjection(state, stored, () => {
|
|
3019
|
+
const current = stored.pendingReversion;
|
|
3020
|
+
if (current?.kind !== 'capture' || current.captureId !== pending.captureId) {
|
|
3021
|
+
throw new FlexHarnessValidationError('Pending capture changed before finalization.');
|
|
3022
|
+
}
|
|
3023
|
+
current.state = 'finalizing';
|
|
3024
|
+
}, true);
|
|
3025
|
+
}
|
|
3026
|
+
const finalizedOutcome = await this.resolveReversionCaptureOutcome(
|
|
2857
3027
|
state,
|
|
2858
3028
|
scopeId,
|
|
2859
3029
|
scope,
|
|
2860
3030
|
stored.session.sessionId,
|
|
2861
3031
|
pending.runId,
|
|
2862
3032
|
pending.captureId,
|
|
2863
|
-
pending.state,
|
|
3033
|
+
pending.state === 'prepared' ? 'finalizing' : pending.state,
|
|
2864
3034
|
);
|
|
2865
|
-
if (
|
|
3035
|
+
if (finalizedOutcome === undefined) {
|
|
2866
3036
|
await this.mutateProjection(state, stored, () => {
|
|
2867
3037
|
stored.reversionSegments = stored.reversionSegments.filter(
|
|
2868
3038
|
(entry) => entry.runId !== pending.runId,
|
|
@@ -2877,7 +3047,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2877
3047
|
: outcome === 'rejected'
|
|
2878
3048
|
? 'failed'
|
|
2879
3049
|
: 'cancelled';
|
|
2880
|
-
|
|
3050
|
+
this.applyFinalizedReversionOutcome(stored, segment, finalizedOutcome);
|
|
2881
3051
|
segment.eventIds = stored.agentSession.getEvents()
|
|
2882
3052
|
.filter((event) => event.generationId === pending.runId)
|
|
2883
3053
|
.map((event) => event.id);
|
|
@@ -3017,10 +3187,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3017
3187
|
}
|
|
3018
3188
|
|
|
3019
3189
|
public retireScope(scopeId: string): Promise<void> {
|
|
3020
|
-
|
|
3021
|
-
if (invocationOwner?.scopeId === scopeId) {
|
|
3022
|
-
throw this.trustInternalError(new FlexHarnessSlashCommandReentryError(invocationOwner.sessionId));
|
|
3023
|
-
}
|
|
3190
|
+
this.assertNoAmbientSlashCommandTeardown({ scopeId });
|
|
3024
3191
|
this.assertOpen();
|
|
3025
3192
|
validateIdentifier(scopeId, 'scopeId');
|
|
3026
3193
|
const existing = this.scopeRetirements.get(scopeId);
|
|
@@ -3039,13 +3206,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3039
3206
|
}
|
|
3040
3207
|
|
|
3041
3208
|
public async dispose(): Promise<void> {
|
|
3209
|
+
this.assertNoAmbientSlashCommandTeardown();
|
|
3042
3210
|
if (this.disposePromise) return this.disposePromise;
|
|
3043
|
-
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
3044
|
-
if (invocationOwner) {
|
|
3045
|
-
throw this.trustInternalError(
|
|
3046
|
-
new FlexHarnessSlashCommandReentryError(invocationOwner.sessionId),
|
|
3047
|
-
);
|
|
3048
|
-
}
|
|
3049
3211
|
this.closed = true;
|
|
3050
3212
|
let disposal!: Promise<void>;
|
|
3051
3213
|
disposal = this.disposeInternal().catch((error) => {
|
|
@@ -3339,6 +3501,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3339
3501
|
const captureId = this.turnReversionProvider
|
|
3340
3502
|
? this.reversionId('capture', run.state.storageKey, run.sessionId, run.runId)
|
|
3341
3503
|
: undefined;
|
|
3504
|
+
const protocolVersion = this.reversionProviderProtocolVersion() ?? 1;
|
|
3342
3505
|
run.stored.reversionSegments.push({
|
|
3343
3506
|
runId: run.runId,
|
|
3344
3507
|
userMessageId: reservation.userMessage.messageId,
|
|
@@ -3346,6 +3509,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3346
3509
|
contextAvailable: true,
|
|
3347
3510
|
eventIds: [],
|
|
3348
3511
|
workspaceCaptured: captureId !== undefined,
|
|
3512
|
+
protocolVersion,
|
|
3513
|
+
provenance: captureId === undefined ? 'transcript' : 'workspace',
|
|
3514
|
+
...(captureId === undefined ? {} : { disposition: 'pending' }),
|
|
3349
3515
|
...(captureId === undefined ? {} : { captureId }),
|
|
3350
3516
|
});
|
|
3351
3517
|
if (captureId) {
|
|
@@ -3354,6 +3520,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3354
3520
|
runId: run.runId,
|
|
3355
3521
|
captureId,
|
|
3356
3522
|
state: 'preparing',
|
|
3523
|
+
protocolVersion,
|
|
3357
3524
|
};
|
|
3358
3525
|
}
|
|
3359
3526
|
}
|
|
@@ -4445,6 +4612,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4445
4612
|
return `${kind}_${sha256Hex(JSON.stringify([storageKey, sessionId, value]))}`;
|
|
4446
4613
|
}
|
|
4447
4614
|
|
|
4615
|
+
private reversionProviderProtocolVersion(): 1 | 2 | undefined {
|
|
4616
|
+
if (!this.turnReversionProvider) return undefined;
|
|
4617
|
+
return 'protocolVersion' in this.turnReversionProvider ? 2 : 1;
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4448
4620
|
private reversionCaptureContext(
|
|
4449
4621
|
run: IActiveRun,
|
|
4450
4622
|
captureId: string,
|
|
@@ -4491,9 +4663,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4491
4663
|
sessionId: string,
|
|
4492
4664
|
runId: string,
|
|
4493
4665
|
captureId: string,
|
|
4494
|
-
) {
|
|
4495
|
-
return this.withReversionMaintenanceSignal((signal) =>
|
|
4496
|
-
this.turnReversionProvider
|
|
4666
|
+
): Promise<TNormalizedCaptureInspection> {
|
|
4667
|
+
return this.withReversionMaintenanceSignal(async (signal) => {
|
|
4668
|
+
const provider = this.turnReversionProvider!;
|
|
4669
|
+
const inspection = await provider.inspectCapture(Object.freeze({
|
|
4497
4670
|
scopeId,
|
|
4498
4671
|
scope,
|
|
4499
4672
|
storageKey,
|
|
@@ -4501,7 +4674,29 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4501
4674
|
runId,
|
|
4502
4675
|
captureId,
|
|
4503
4676
|
signal,
|
|
4504
|
-
}))
|
|
4677
|
+
}));
|
|
4678
|
+
if (inspection.status !== 'finalized') return { status: inspection.status };
|
|
4679
|
+
if (this.reversionProviderProtocolVersion() === 2) {
|
|
4680
|
+
if (!('outcome' in inspection)) {
|
|
4681
|
+
throw new FlexHarnessValidationError('Protocol-2 finalized inspection has no outcome.');
|
|
4682
|
+
}
|
|
4683
|
+
return {
|
|
4684
|
+
status: 'finalized',
|
|
4685
|
+
outcome: this.normalizeReversionOutcome(inspection.outcome),
|
|
4686
|
+
};
|
|
4687
|
+
}
|
|
4688
|
+
if (!('reference' in inspection) || inspection.reference === undefined) {
|
|
4689
|
+
throw new FlexHarnessValidationError('Protocol-1 finalized inspection has no reference.');
|
|
4690
|
+
}
|
|
4691
|
+
return {
|
|
4692
|
+
status: 'finalized',
|
|
4693
|
+
outcome: {
|
|
4694
|
+
disposition: 'revertible',
|
|
4695
|
+
reference: this.normalizeReversionReference(inspection.reference),
|
|
4696
|
+
affectedWorkspaces: [],
|
|
4697
|
+
},
|
|
4698
|
+
};
|
|
4699
|
+
});
|
|
4505
4700
|
}
|
|
4506
4701
|
|
|
4507
4702
|
private finalizeReversionCapture(
|
|
@@ -4511,9 +4706,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4511
4706
|
sessionId: string,
|
|
4512
4707
|
runId: string,
|
|
4513
4708
|
captureId: string,
|
|
4514
|
-
) {
|
|
4515
|
-
return this.withReversionMaintenanceSignal((signal) =>
|
|
4516
|
-
this.turnReversionProvider!.finalize(Object.freeze({
|
|
4709
|
+
): Promise<TFlexTurnReversionFinalizedOutcome> {
|
|
4710
|
+
return this.withReversionMaintenanceSignal(async (signal) => {
|
|
4711
|
+
const result = await this.turnReversionProvider!.finalize(Object.freeze({
|
|
4517
4712
|
scopeId,
|
|
4518
4713
|
scope,
|
|
4519
4714
|
storageKey,
|
|
@@ -4521,10 +4716,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4521
4716
|
runId,
|
|
4522
4717
|
captureId,
|
|
4523
4718
|
signal,
|
|
4524
|
-
}))
|
|
4719
|
+
}));
|
|
4720
|
+
return this.reversionProviderProtocolVersion() === 2
|
|
4721
|
+
? this.normalizeReversionOutcome(result)
|
|
4722
|
+
: {
|
|
4723
|
+
disposition: 'revertible',
|
|
4724
|
+
reference: this.normalizeReversionReference(result),
|
|
4725
|
+
affectedWorkspaces: [],
|
|
4726
|
+
};
|
|
4727
|
+
});
|
|
4525
4728
|
}
|
|
4526
4729
|
|
|
4527
|
-
private async
|
|
4730
|
+
private async resolveReversionCaptureOutcome(
|
|
4528
4731
|
state: IStorageState,
|
|
4529
4732
|
scopeId: string,
|
|
4530
4733
|
scope: TScope,
|
|
@@ -4532,7 +4735,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4532
4735
|
runId: string,
|
|
4533
4736
|
captureId: string,
|
|
4534
4737
|
durableState: 'preparing' | 'prepared' | 'finalizing',
|
|
4535
|
-
): Promise<
|
|
4738
|
+
): Promise<TFlexTurnReversionFinalizedOutcome | undefined> {
|
|
4536
4739
|
let inspection;
|
|
4537
4740
|
try {
|
|
4538
4741
|
inspection = await this.inspectReversionCapture(
|
|
@@ -4556,10 +4759,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4556
4759
|
state.lifecycle = 'fenced';
|
|
4557
4760
|
throw new FlexHarnessValidationError('Prepared reversion capture is missing.');
|
|
4558
4761
|
}
|
|
4559
|
-
let
|
|
4762
|
+
let outcome = inspection.status === 'finalized' ? inspection.outcome : undefined;
|
|
4560
4763
|
if (inspection.status === 'prepared') {
|
|
4561
4764
|
try {
|
|
4562
|
-
|
|
4765
|
+
outcome = await this.finalizeReversionCapture(
|
|
4563
4766
|
scopeId,
|
|
4564
4767
|
scope,
|
|
4565
4768
|
state.storageKey,
|
|
@@ -4576,18 +4779,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4576
4779
|
runId,
|
|
4577
4780
|
captureId,
|
|
4578
4781
|
);
|
|
4579
|
-
if (finalInspection.status !== 'finalized'
|
|
4782
|
+
if (finalInspection.status !== 'finalized') {
|
|
4580
4783
|
if (finalInspection.status === 'unknown') state.lifecycle = 'fenced';
|
|
4581
4784
|
throw this.projectOperationError(error, 'turnReversion', scopeId, sessionId, captureId);
|
|
4582
4785
|
}
|
|
4583
|
-
|
|
4786
|
+
outcome = finalInspection.outcome;
|
|
4584
4787
|
}
|
|
4585
4788
|
}
|
|
4586
|
-
if (
|
|
4789
|
+
if (outcome === undefined) {
|
|
4587
4790
|
state.lifecycle = 'fenced';
|
|
4588
|
-
throw new FlexHarnessValidationError('Finalized reversion capture has no
|
|
4791
|
+
throw new FlexHarnessValidationError('Finalized reversion capture has no outcome.');
|
|
4589
4792
|
}
|
|
4590
|
-
return
|
|
4793
|
+
return outcome;
|
|
4591
4794
|
}
|
|
4592
4795
|
|
|
4593
4796
|
private normalizeReversionReference(reference: unknown): TJsonValue {
|
|
@@ -4600,6 +4803,102 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4600
4803
|
});
|
|
4601
4804
|
}
|
|
4602
4805
|
|
|
4806
|
+
private normalizeReversionOutcome(value: unknown): TFlexTurnReversionFinalizedOutcome {
|
|
4807
|
+
if (
|
|
4808
|
+
!value
|
|
4809
|
+
|| typeof value !== 'object'
|
|
4810
|
+
|| Array.isArray(value)
|
|
4811
|
+
|| (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
4812
|
+
) throw new FlexHarnessValidationError('Turn reversion finalized outcome must be a plain object.');
|
|
4813
|
+
const outcome = value as Record<string, unknown>;
|
|
4814
|
+
const disposition = outcome.disposition;
|
|
4815
|
+
if (!['revertible', 'no-change', 'nonrevertible'].includes(String(disposition))) {
|
|
4816
|
+
throw new FlexHarnessValidationError('Turn reversion finalized outcome disposition is invalid.');
|
|
4817
|
+
}
|
|
4818
|
+
const allowedKeys = disposition === 'nonrevertible'
|
|
4819
|
+
? ['disposition', 'reference', 'reasonCode', 'affectedWorkspaces']
|
|
4820
|
+
: ['disposition', 'reference', 'affectedWorkspaces'];
|
|
4821
|
+
const unsupported = Object.keys(outcome).find((key) => !allowedKeys.includes(key));
|
|
4822
|
+
if (unsupported) {
|
|
4823
|
+
throw new FlexHarnessValidationError(
|
|
4824
|
+
`Turn reversion finalized outcome does not support "${unsupported}".`,
|
|
4825
|
+
);
|
|
4826
|
+
}
|
|
4827
|
+
if (!Object.prototype.hasOwnProperty.call(outcome, 'reference')) {
|
|
4828
|
+
throw new FlexHarnessValidationError('Turn reversion finalized outcome requires a reference.');
|
|
4829
|
+
}
|
|
4830
|
+
const reference = this.normalizeReversionReference(outcome.reference);
|
|
4831
|
+
const affectedWorkspaces = outcome.affectedWorkspaces === undefined
|
|
4832
|
+
? undefined
|
|
4833
|
+
: this.normalizeAffectedWorkspaces(outcome.affectedWorkspaces);
|
|
4834
|
+
if (disposition === 'revertible') {
|
|
4835
|
+
if (affectedWorkspaces === undefined) {
|
|
4836
|
+
throw new FlexHarnessValidationError('Revertible outcome requires affectedWorkspaces.');
|
|
4837
|
+
}
|
|
4838
|
+
return { disposition, reference, affectedWorkspaces };
|
|
4839
|
+
}
|
|
4840
|
+
if (disposition === 'no-change') {
|
|
4841
|
+
if (affectedWorkspaces && affectedWorkspaces.length > 0) {
|
|
4842
|
+
throw new FlexHarnessValidationError('No-change outcome cannot affect a workspace.');
|
|
4843
|
+
}
|
|
4844
|
+
return {
|
|
4845
|
+
disposition,
|
|
4846
|
+
reference,
|
|
4847
|
+
...(affectedWorkspaces === undefined ? {} : { affectedWorkspaces: [] }),
|
|
4848
|
+
};
|
|
4849
|
+
}
|
|
4850
|
+
if (
|
|
4851
|
+
typeof outcome.reasonCode !== 'string'
|
|
4852
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(outcome.reasonCode)
|
|
4853
|
+
|| Buffer.byteLength(outcome.reasonCode, 'utf8') > FLEX_REVERSION_REASON_CODE_MAX_BYTES
|
|
4854
|
+
) throw new FlexHarnessValidationError('Nonrevertible outcome reasonCode is invalid.');
|
|
4855
|
+
return {
|
|
4856
|
+
disposition: 'nonrevertible',
|
|
4857
|
+
reference,
|
|
4858
|
+
reasonCode: outcome.reasonCode,
|
|
4859
|
+
...(affectedWorkspaces === undefined ? {} : { affectedWorkspaces }),
|
|
4860
|
+
};
|
|
4861
|
+
}
|
|
4862
|
+
|
|
4863
|
+
private normalizeAffectedWorkspaces(value: unknown): IFlexAffectedWorkspace[] {
|
|
4864
|
+
if (!Array.isArray(value) || value.length > FLEX_REVERSION_MAX_AFFECTED_WORKSPACES) {
|
|
4865
|
+
throw new FlexHarnessValidationError(
|
|
4866
|
+
`affectedWorkspaces must contain at most ${FLEX_REVERSION_MAX_AFFECTED_WORKSPACES} entries.`,
|
|
4867
|
+
);
|
|
4868
|
+
}
|
|
4869
|
+
const ids = new Set<string>();
|
|
4870
|
+
return value.map((entry, index) => {
|
|
4871
|
+
if (
|
|
4872
|
+
!entry
|
|
4873
|
+
|| typeof entry !== 'object'
|
|
4874
|
+
|| Array.isArray(entry)
|
|
4875
|
+
|| (Object.getPrototypeOf(entry) !== Object.prototype && Object.getPrototypeOf(entry) !== null)
|
|
4876
|
+
) throw new FlexHarnessValidationError(`affectedWorkspaces[${index}] must be a plain object.`);
|
|
4877
|
+
const descriptor = entry as Record<string, unknown>;
|
|
4878
|
+
const unsupported = Object.keys(descriptor).find((key) => !['id', 'label'].includes(key));
|
|
4879
|
+
if (unsupported) {
|
|
4880
|
+
throw new FlexHarnessValidationError(
|
|
4881
|
+
`affectedWorkspaces[${index}] does not support "${unsupported}".`,
|
|
4882
|
+
);
|
|
4883
|
+
}
|
|
4884
|
+
if (
|
|
4885
|
+
typeof descriptor.id !== 'string'
|
|
4886
|
+
|| !descriptor.id.trim()
|
|
4887
|
+
|| Buffer.byteLength(descriptor.id, 'utf8') > FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES
|
|
4888
|
+
) throw new FlexHarnessValidationError(`affectedWorkspaces[${index}].id is invalid.`);
|
|
4889
|
+
if (
|
|
4890
|
+
typeof descriptor.label !== 'string'
|
|
4891
|
+
|| !descriptor.label.trim()
|
|
4892
|
+
|| Buffer.byteLength(descriptor.label, 'utf8') > FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES
|
|
4893
|
+
) throw new FlexHarnessValidationError(`affectedWorkspaces[${index}].label is invalid.`);
|
|
4894
|
+
if (ids.has(descriptor.id)) {
|
|
4895
|
+
throw new FlexHarnessValidationError(`affectedWorkspaces contains duplicate id "${descriptor.id}".`);
|
|
4896
|
+
}
|
|
4897
|
+
ids.add(descriptor.id);
|
|
4898
|
+
return { id: descriptor.id, label: descriptor.label };
|
|
4899
|
+
});
|
|
4900
|
+
}
|
|
4901
|
+
|
|
4603
4902
|
private async prepareRunReversion(run: IActiveRun, signal: AbortSignal): Promise<void> {
|
|
4604
4903
|
if (run.stored.session.agent !== undefined || !this.turnReversionProvider) return;
|
|
4605
4904
|
const segment = run.stored.reversionSegments.find((entry) => entry.runId === run.runId);
|
|
@@ -4612,6 +4911,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4612
4911
|
);
|
|
4613
4912
|
}
|
|
4614
4913
|
const context = this.reversionCaptureContext(run, captureId, signal);
|
|
4914
|
+
if (this.reversionProviderProtocolVersion() !== segment.protocolVersion) {
|
|
4915
|
+
throw this.projectExternalError(
|
|
4916
|
+
run,
|
|
4917
|
+
new Error(`Root generation requires reversion protocolVersion ${segment.protocolVersion}.`),
|
|
4918
|
+
'turnReversion',
|
|
4919
|
+
);
|
|
4920
|
+
}
|
|
4615
4921
|
try {
|
|
4616
4922
|
await this.turnReversionProvider.prepare(context);
|
|
4617
4923
|
} catch (error) {
|
|
@@ -4653,7 +4959,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4653
4959
|
const eventIds = run.stored.agentSession.getEvents()
|
|
4654
4960
|
.filter((event) => event.generationId === run.runId)
|
|
4655
4961
|
.map((event) => event.id);
|
|
4656
|
-
if (!this.turnReversionProvider ||
|
|
4962
|
+
if (!this.turnReversionProvider || segment.provenance === 'transcript') {
|
|
4657
4963
|
await this.mutateProjection(run.state, run.stored, () => {
|
|
4658
4964
|
const wasCompleted = segment.status === 'completed';
|
|
4659
4965
|
segment.status = status;
|
|
@@ -4665,6 +4971,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4665
4971
|
}, true);
|
|
4666
4972
|
return;
|
|
4667
4973
|
}
|
|
4974
|
+
if (!segment.captureId) {
|
|
4975
|
+
throw new FlexHarnessValidationError('Workspace reversion segment has no capture ownership.');
|
|
4976
|
+
}
|
|
4668
4977
|
const captureId = segment.captureId;
|
|
4669
4978
|
if (segment.workspaceReference !== undefined) {
|
|
4670
4979
|
await this.mutateProjection(run.state, run.stored, () => {
|
|
@@ -4681,16 +4990,30 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4681
4990
|
if (pending?.kind !== 'capture' || pending.runId !== run.runId) {
|
|
4682
4991
|
throw new FlexHarnessValidationError('Reversion capture intent changed before finalization.');
|
|
4683
4992
|
}
|
|
4684
|
-
|
|
4993
|
+
if (this.reversionProviderProtocolVersion() !== pending.protocolVersion) {
|
|
4994
|
+
throw new FlexHarnessValidationError(
|
|
4995
|
+
`Reversion capture finalization requires protocolVersion ${pending.protocolVersion}.`,
|
|
4996
|
+
);
|
|
4997
|
+
}
|
|
4998
|
+
if (pending.state === 'prepared') {
|
|
4999
|
+
await this.mutateProjection(run.state, run.stored, () => {
|
|
5000
|
+
const current = run.stored.pendingReversion;
|
|
5001
|
+
if (current?.kind !== 'capture' || current.captureId !== captureId) {
|
|
5002
|
+
throw new FlexHarnessValidationError('Reversion capture changed before finalization.');
|
|
5003
|
+
}
|
|
5004
|
+
current.state = 'finalizing';
|
|
5005
|
+
}, true);
|
|
5006
|
+
}
|
|
5007
|
+
const outcome = await this.resolveReversionCaptureOutcome(
|
|
4685
5008
|
run.state,
|
|
4686
5009
|
run.scopeId,
|
|
4687
5010
|
run.scope as TScope,
|
|
4688
5011
|
run.sessionId,
|
|
4689
5012
|
run.runId,
|
|
4690
5013
|
captureId,
|
|
4691
|
-
pending.state,
|
|
5014
|
+
pending.state === 'prepared' ? 'finalizing' : pending.state,
|
|
4692
5015
|
);
|
|
4693
|
-
if (
|
|
5016
|
+
if (outcome === undefined) {
|
|
4694
5017
|
await this.mutateProjection(run.state, run.stored, () => {
|
|
4695
5018
|
run.stored.reversionSegments = run.stored.reversionSegments.filter(
|
|
4696
5019
|
(entry) => entry.runId !== run.runId,
|
|
@@ -4707,7 +5030,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4707
5030
|
const wasCompleted = segment.status === 'completed';
|
|
4708
5031
|
segment.status = status;
|
|
4709
5032
|
segment.eventIds = eventIds;
|
|
4710
|
-
|
|
5033
|
+
this.applyFinalizedReversionOutcome(run.stored, segment, outcome);
|
|
4711
5034
|
if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
|
|
4712
5035
|
else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
|
|
4713
5036
|
delete run.stored.pendingReversion;
|
|
@@ -4715,34 +5038,73 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4715
5038
|
}, true);
|
|
4716
5039
|
}
|
|
4717
5040
|
|
|
5041
|
+
private applyFinalizedReversionOutcome(
|
|
5042
|
+
stored: IStoredSessionState,
|
|
5043
|
+
segment: IFlexReversionSegment,
|
|
5044
|
+
outcome: TFlexTurnReversionFinalizedOutcome,
|
|
5045
|
+
): void {
|
|
5046
|
+
const captureId = segment.captureId;
|
|
5047
|
+
if (!captureId) {
|
|
5048
|
+
throw new FlexHarnessValidationError('Finalized workspace outcome has no capture ownership.');
|
|
5049
|
+
}
|
|
5050
|
+
segment.disposition = outcome.disposition;
|
|
5051
|
+
delete segment.affectedWorkspaces;
|
|
5052
|
+
delete segment.reasonCode;
|
|
5053
|
+
if (segment.protocolVersion === 2) {
|
|
5054
|
+
if (outcome.affectedWorkspaces !== undefined) {
|
|
5055
|
+
segment.affectedWorkspaces = cloneSerializable([...outcome.affectedWorkspaces]);
|
|
5056
|
+
}
|
|
5057
|
+
if (outcome.disposition === 'nonrevertible') segment.reasonCode = outcome.reasonCode;
|
|
5058
|
+
}
|
|
5059
|
+
if (outcome.disposition === 'revertible') {
|
|
5060
|
+
segment.workspaceReference = cloneSerializable(outcome.reference);
|
|
5061
|
+
return;
|
|
5062
|
+
}
|
|
5063
|
+
this.enqueueReversionReleaseReference(stored, {
|
|
5064
|
+
runId: segment.runId,
|
|
5065
|
+
captureId,
|
|
5066
|
+
reference: cloneSerializable(outcome.reference),
|
|
5067
|
+
protocolVersion: segment.protocolVersion,
|
|
5068
|
+
});
|
|
5069
|
+
delete segment.captureId;
|
|
5070
|
+
delete segment.workspaceReference;
|
|
5071
|
+
}
|
|
5072
|
+
|
|
4718
5073
|
private hiddenReversionSegments(stored: IStoredSessionState): IFlexReversionSegment[] {
|
|
4719
|
-
const
|
|
4720
|
-
const firstHidden = candidates[stored.revertCursor];
|
|
5074
|
+
const firstHidden = this.reversionGroups(stored)[stored.revertCursor]?.segments[0];
|
|
4721
5075
|
if (!firstHidden) return [];
|
|
4722
5076
|
const index = stored.reversionSegments.findIndex((segment) => segment.runId === firstHidden.runId);
|
|
4723
5077
|
if (index < 0) return [];
|
|
4724
|
-
return stored.reversionSegments.slice(
|
|
5078
|
+
return stored.reversionSegments.slice(index);
|
|
4725
5079
|
}
|
|
4726
5080
|
|
|
4727
5081
|
private enqueueReversionRelease(
|
|
4728
5082
|
stored: IStoredSessionState,
|
|
4729
5083
|
segment: IFlexReversionSegment,
|
|
4730
5084
|
): void {
|
|
4731
|
-
if (
|
|
5085
|
+
if (segment.disposition !== 'revertible') return;
|
|
4732
5086
|
if (segment.captureId === undefined || segment.workspaceReference === undefined) {
|
|
4733
5087
|
throw new FlexHarnessValidationError('Capture-backed segment has no releasable workspace reference.');
|
|
4734
5088
|
}
|
|
4735
|
-
|
|
5089
|
+
this.enqueueReversionReleaseReference(stored, {
|
|
5090
|
+
runId: segment.runId,
|
|
5091
|
+
captureId: segment.captureId,
|
|
5092
|
+
reference: cloneSerializable(segment.workspaceReference),
|
|
5093
|
+
protocolVersion: segment.protocolVersion,
|
|
5094
|
+
});
|
|
5095
|
+
}
|
|
5096
|
+
|
|
5097
|
+
private enqueueReversionReleaseReference(
|
|
5098
|
+
stored: IStoredSessionState,
|
|
5099
|
+
release: IFlexPendingReversionRelease,
|
|
5100
|
+
): void {
|
|
5101
|
+
if (stored.pendingReversionReleases.some((entry) => entry.captureId === release.captureId)) {
|
|
4736
5102
|
return;
|
|
4737
5103
|
}
|
|
4738
5104
|
if (stored.pendingReversionReleases.length >= this.reversionLimits.maxPendingReversionReleases) {
|
|
4739
5105
|
throw new FlexHarnessValidationError('Pending reversion release limit was reached.');
|
|
4740
5106
|
}
|
|
4741
|
-
stored.pendingReversionReleases.push(
|
|
4742
|
-
runId: segment.runId,
|
|
4743
|
-
captureId: segment.captureId,
|
|
4744
|
-
reference: cloneSerializable(segment.workspaceReference),
|
|
4745
|
-
});
|
|
5107
|
+
stored.pendingReversionReleases.push(cloneSerializable(release));
|
|
4746
5108
|
}
|
|
4747
5109
|
|
|
4748
5110
|
private pruneReversionState(
|
|
@@ -4784,7 +5146,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4784
5146
|
}
|
|
4785
5147
|
if (end <= 0) break;
|
|
4786
5148
|
const prefix = stored.reversionSegments.slice(0, end);
|
|
4787
|
-
if (prefix.some((segment) =>
|
|
5149
|
+
if (prefix.some((segment) =>
|
|
5150
|
+
segment.disposition === 'revertible' && segment.workspaceReference === undefined)) break;
|
|
4788
5151
|
for (const segment of prefix) this.enqueueReversionRelease(stored, segment);
|
|
4789
5152
|
stored.reversionSegments.splice(0, end);
|
|
4790
5153
|
stored.revertCursor = Math.max(
|
|
@@ -4897,6 +5260,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4897
5260
|
}
|
|
4898
5261
|
while (stored.pendingReversionReleases.length > 0) {
|
|
4899
5262
|
const release = stored.pendingReversionReleases[0];
|
|
5263
|
+
if (this.reversionProviderProtocolVersion() !== release.protocolVersion) {
|
|
5264
|
+
throw new FlexHarnessValidationError(
|
|
5265
|
+
`Pending workspace release requires protocolVersion ${release.protocolVersion}.`,
|
|
5266
|
+
);
|
|
5267
|
+
}
|
|
4900
5268
|
try {
|
|
4901
5269
|
await this.withReversionMaintenanceSignal((signal) =>
|
|
4902
5270
|
this.turnReversionProvider!.release(Object.freeze({
|
|
@@ -4947,9 +5315,33 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4947
5315
|
stored?: IStoredSessionState,
|
|
4948
5316
|
): Promise<void> {
|
|
4949
5317
|
if (stored) await stored.projectionQueue;
|
|
4950
|
-
|
|
4951
|
-
if (
|
|
4952
|
-
let projection
|
|
5318
|
+
let durableProjection = await this.stores.projections.load(storageKey, sessionId);
|
|
5319
|
+
if (!durableProjection) return;
|
|
5320
|
+
let projection = this.upgradeProjectionSnapshot(durableProjection);
|
|
5321
|
+
const persistDirect = async (next: IFlexProjectionSnapshotCurrent): Promise<void> => {
|
|
5322
|
+
const before = durableProjection;
|
|
5323
|
+
try {
|
|
5324
|
+
await this.stores.projections.save(storageKey, sessionId, next, projection.revision);
|
|
5325
|
+
durableProjection = next;
|
|
5326
|
+
projection = next;
|
|
5327
|
+
} catch (error) {
|
|
5328
|
+
let current: TFlexProjectionSnapshot | undefined;
|
|
5329
|
+
let reconciliationError: unknown;
|
|
5330
|
+
try {
|
|
5331
|
+
current = await this.stores.projections.load(storageKey, sessionId);
|
|
5332
|
+
} catch (loadError) {
|
|
5333
|
+
reconciliationError = loadError;
|
|
5334
|
+
}
|
|
5335
|
+
if (current && JSON.stringify(current) === JSON.stringify(next)) {
|
|
5336
|
+
durableProjection = current;
|
|
5337
|
+
projection = next;
|
|
5338
|
+
return;
|
|
5339
|
+
}
|
|
5340
|
+
if (current && before && JSON.stringify(current) === JSON.stringify(before)) throw error;
|
|
5341
|
+
state.lifecycle = 'fenced';
|
|
5342
|
+
throw reconciliationError === undefined ? error : combineErrors([error, reconciliationError]);
|
|
5343
|
+
}
|
|
5344
|
+
};
|
|
4953
5345
|
if (projection.pendingReversion?.kind === 'apply') {
|
|
4954
5346
|
if (stored) {
|
|
4955
5347
|
await this.resumePendingApply(state, stored, scopeId, scope);
|
|
@@ -4961,12 +5353,17 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4961
5353
|
for (const runId of orderedRunIds) {
|
|
4962
5354
|
if (pending.appliedRunIds.includes(runId)) continue;
|
|
4963
5355
|
const segment = projection.reversionSegments.find((entry) => entry.runId === runId)!;
|
|
4964
|
-
if (segment.
|
|
5356
|
+
if (segment.disposition === 'revertible') {
|
|
4965
5357
|
if (!this.turnReversionProvider || !segment.captureId || segment.workspaceReference === undefined) {
|
|
4966
5358
|
throw new FlexHarnessValidationError(
|
|
4967
5359
|
'Pending workspace reversion deletion requires its turn reversion provider.',
|
|
4968
5360
|
);
|
|
4969
5361
|
}
|
|
5362
|
+
if (this.reversionProviderProtocolVersion() !== segment.protocolVersion) {
|
|
5363
|
+
throw new FlexHarnessValidationError(
|
|
5364
|
+
`Pending workspace reversion deletion requires protocolVersion ${segment.protocolVersion}.`,
|
|
5365
|
+
);
|
|
5366
|
+
}
|
|
4970
5367
|
const captureId = segment.captureId;
|
|
4971
5368
|
const reference = segment.workspaceReference;
|
|
4972
5369
|
const operationId = `${pending.operationId}:${sha256Hex(runId).slice(0, 16)}`;
|
|
@@ -4985,6 +5382,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4985
5382
|
let inspection = await this.withReversionMaintenanceSignal((signal) =>
|
|
4986
5383
|
this.turnReversionProvider!.inspectApply(createContext(signal)));
|
|
4987
5384
|
if (inspection.status === 'unknown') {
|
|
5385
|
+
state.lifecycle = 'fenced';
|
|
4988
5386
|
throw new FlexHarnessValidationError('Workspace reversion apply outcome is unknown.');
|
|
4989
5387
|
}
|
|
4990
5388
|
if (inspection.status === 'not-applied') {
|
|
@@ -4994,14 +5392,15 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4994
5392
|
} catch (error) {
|
|
4995
5393
|
inspection = await this.withReversionMaintenanceSignal((signal) =>
|
|
4996
5394
|
this.turnReversionProvider!.inspectApply(createContext(signal)));
|
|
4997
|
-
if (inspection.status !== 'applied')
|
|
5395
|
+
if (inspection.status !== 'applied') {
|
|
5396
|
+
if (inspection.status === 'unknown') state.lifecycle = 'fenced';
|
|
5397
|
+
throw error;
|
|
5398
|
+
}
|
|
4998
5399
|
}
|
|
4999
5400
|
}
|
|
5000
5401
|
}
|
|
5001
5402
|
pending.appliedRunIds.push(runId);
|
|
5002
|
-
|
|
5003
|
-
await this.stores.projections.save(storageKey, sessionId, next, projection.revision);
|
|
5004
|
-
projection = next;
|
|
5403
|
+
await persistDirect({ ...projection, revision: projection.revision + 1 });
|
|
5005
5404
|
}
|
|
5006
5405
|
const { pendingReversion: _pendingReversion, ...completedProjection } = projection;
|
|
5007
5406
|
const completed: IFlexProjectionSnapshotCurrent = {
|
|
@@ -5009,23 +5408,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5009
5408
|
revision: projection.revision + 1,
|
|
5010
5409
|
revertCursor: pending.toCursor,
|
|
5011
5410
|
};
|
|
5012
|
-
await
|
|
5013
|
-
projection = completed;
|
|
5411
|
+
await persistDirect(completed);
|
|
5014
5412
|
}
|
|
5015
5413
|
const recoveredProjection = await this.stores.projections.load(storageKey, sessionId);
|
|
5016
|
-
if (recoveredProjection
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
const hasWorkspaceCaptures = projection.reversionSegments.some((segment) => segment.captureId)
|
|
5020
|
-
|| projection.pendingReversionReleases.length > 0
|
|
5021
|
-
|| projection.pendingReversion?.kind === 'capture';
|
|
5022
|
-
if (!this.turnReversionProvider) {
|
|
5023
|
-
if (hasWorkspaceCaptures) {
|
|
5024
|
-
throw new FlexHarnessValidationError(
|
|
5025
|
-
'Capture-backed session deletion requires its turn reversion provider.',
|
|
5026
|
-
);
|
|
5027
|
-
}
|
|
5028
|
-
return;
|
|
5414
|
+
if (!recoveredProjection) return;
|
|
5415
|
+
durableProjection = recoveredProjection;
|
|
5416
|
+
projection = this.upgradeProjectionSnapshot(recoveredProjection);
|
|
5029
5417
|
}
|
|
5030
5418
|
const releases = new Map<string, IFlexPendingReversionRelease>();
|
|
5031
5419
|
for (const release of projection.pendingReversionReleases) {
|
|
@@ -5037,12 +5425,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5037
5425
|
runId: segment.runId,
|
|
5038
5426
|
captureId: segment.captureId,
|
|
5039
5427
|
reference: cloneSerializable(segment.workspaceReference),
|
|
5428
|
+
protocolVersion: segment.protocolVersion,
|
|
5040
5429
|
});
|
|
5041
5430
|
}
|
|
5042
5431
|
}
|
|
5043
5432
|
const unresolvedCaptures = new Map<string, {
|
|
5044
5433
|
runId: string;
|
|
5045
5434
|
captureId: string;
|
|
5435
|
+
protocolVersion: 1 | 2;
|
|
5046
5436
|
durableState?: 'preparing' | 'prepared' | 'finalizing';
|
|
5047
5437
|
}>();
|
|
5048
5438
|
for (const segment of projection.reversionSegments) {
|
|
@@ -5050,6 +5440,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5050
5440
|
unresolvedCaptures.set(segment.captureId, {
|
|
5051
5441
|
runId: segment.runId,
|
|
5052
5442
|
captureId: segment.captureId,
|
|
5443
|
+
protocolVersion: segment.protocolVersion,
|
|
5053
5444
|
});
|
|
5054
5445
|
}
|
|
5055
5446
|
}
|
|
@@ -5057,10 +5448,36 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5057
5448
|
unresolvedCaptures.set(projection.pendingReversion.captureId, {
|
|
5058
5449
|
runId: projection.pendingReversion.runId,
|
|
5059
5450
|
captureId: projection.pendingReversion.captureId,
|
|
5451
|
+
protocolVersion: projection.pendingReversion.protocolVersion,
|
|
5060
5452
|
durableState: projection.pendingReversion.state,
|
|
5061
5453
|
});
|
|
5062
5454
|
}
|
|
5455
|
+
if (unresolvedCaptures.size > 0 && !this.turnReversionProvider) {
|
|
5456
|
+
throw new FlexHarnessValidationError(
|
|
5457
|
+
'Capture-backed session deletion requires its turn reversion provider.',
|
|
5458
|
+
);
|
|
5459
|
+
}
|
|
5063
5460
|
for (const capture of unresolvedCaptures.values()) {
|
|
5461
|
+
if (this.reversionProviderProtocolVersion() !== capture.protocolVersion) {
|
|
5462
|
+
throw new FlexHarnessValidationError(
|
|
5463
|
+
`Deleted session capture requires protocolVersion ${capture.protocolVersion}.`,
|
|
5464
|
+
);
|
|
5465
|
+
}
|
|
5466
|
+
if (capture.durableState === 'prepared') {
|
|
5467
|
+
if (stored) {
|
|
5468
|
+
await this.mutateProjection(state, stored, () => {
|
|
5469
|
+
const pending = stored.pendingReversion;
|
|
5470
|
+
if (pending?.kind !== 'capture' || pending.captureId !== capture.captureId) {
|
|
5471
|
+
throw new FlexHarnessValidationError('Deleted session capture changed before finalization.');
|
|
5472
|
+
}
|
|
5473
|
+
pending.state = 'finalizing';
|
|
5474
|
+
}, true);
|
|
5475
|
+
} else if (projection.pendingReversion?.kind === 'capture') {
|
|
5476
|
+
projection.pendingReversion.state = 'finalizing';
|
|
5477
|
+
await persistDirect({ ...projection, revision: projection.revision + 1 });
|
|
5478
|
+
}
|
|
5479
|
+
capture.durableState = 'finalizing';
|
|
5480
|
+
}
|
|
5064
5481
|
let inspection;
|
|
5065
5482
|
try {
|
|
5066
5483
|
inspection = await this.inspectReversionCapture(
|
|
@@ -5081,6 +5498,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5081
5498
|
);
|
|
5082
5499
|
}
|
|
5083
5500
|
if (inspection.status === 'unknown') {
|
|
5501
|
+
state.lifecycle = 'fenced';
|
|
5084
5502
|
throw new FlexHarnessValidationError('Deleted session capture outcome is unknown.');
|
|
5085
5503
|
}
|
|
5086
5504
|
if (inspection.status === 'missing') {
|
|
@@ -5088,10 +5506,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5088
5506
|
state.lifecycle = 'fenced';
|
|
5089
5507
|
throw new FlexHarnessValidationError('Prepared deleted-session capture is missing.');
|
|
5090
5508
|
}
|
|
5091
|
-
let
|
|
5509
|
+
let outcome = inspection.status === 'finalized' ? inspection.outcome : undefined;
|
|
5092
5510
|
if (inspection.status === 'prepared') {
|
|
5093
5511
|
try {
|
|
5094
|
-
|
|
5512
|
+
outcome = await this.finalizeReversionCapture(
|
|
5095
5513
|
scopeId,
|
|
5096
5514
|
scope,
|
|
5097
5515
|
storageKey,
|
|
@@ -5108,7 +5526,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5108
5526
|
capture.runId,
|
|
5109
5527
|
capture.captureId,
|
|
5110
5528
|
);
|
|
5111
|
-
if (finalInspection.status !== 'finalized'
|
|
5529
|
+
if (finalInspection.status !== 'finalized') {
|
|
5530
|
+
if (finalInspection.status === 'unknown') state.lifecycle = 'fenced';
|
|
5112
5531
|
throw this.projectOperationError(
|
|
5113
5532
|
error,
|
|
5114
5533
|
'turnReversion',
|
|
@@ -5117,63 +5536,77 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5117
5536
|
`release:${capture.captureId}`,
|
|
5118
5537
|
);
|
|
5119
5538
|
}
|
|
5120
|
-
|
|
5539
|
+
outcome = finalInspection.outcome;
|
|
5121
5540
|
}
|
|
5122
5541
|
}
|
|
5123
|
-
if (
|
|
5542
|
+
if (outcome === undefined) {
|
|
5124
5543
|
throw new FlexHarnessValidationError('Deleted session capture has no release reference.');
|
|
5125
5544
|
}
|
|
5126
5545
|
releases.set(capture.captureId, {
|
|
5127
5546
|
runId: capture.runId,
|
|
5128
5547
|
captureId: capture.captureId,
|
|
5129
|
-
reference:
|
|
5548
|
+
reference: cloneSerializable(outcome.reference),
|
|
5549
|
+
protocolVersion: capture.protocolVersion,
|
|
5130
5550
|
});
|
|
5131
5551
|
}
|
|
5552
|
+
if (releases.size === 0) return;
|
|
5553
|
+
if (!this.turnReversionProvider) {
|
|
5554
|
+
throw new FlexHarnessValidationError(
|
|
5555
|
+
'Capture-backed session deletion requires its turn reversion provider.',
|
|
5556
|
+
);
|
|
5557
|
+
}
|
|
5132
5558
|
if (stored) {
|
|
5133
5559
|
await this.mutateProjection(state, stored, () => {
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5560
|
+
stored.reversionSegments = [];
|
|
5561
|
+
stored.revertCursor = 0;
|
|
5562
|
+
delete stored.pendingReversion;
|
|
5563
|
+
stored.pendingReversionReleases = [...releases.values()].map((release) =>
|
|
5564
|
+
cloneSerializable(release));
|
|
5139
5565
|
}, true);
|
|
5566
|
+
await this.drainReversionReleases(state, stored, scopeId, scope, true);
|
|
5140
5567
|
} else {
|
|
5141
|
-
const
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5568
|
+
const { pendingReversion: _pendingReversion, ...withoutPending } = projection;
|
|
5569
|
+
await persistDirect({
|
|
5570
|
+
...withoutPending,
|
|
5571
|
+
revision: projection.revision + 1,
|
|
5572
|
+
reversionSegments: [],
|
|
5573
|
+
revertCursor: 0,
|
|
5574
|
+
pendingReversionReleases: [...releases.values()].map((release) =>
|
|
5575
|
+
cloneSerializable(release)),
|
|
5576
|
+
});
|
|
5577
|
+
while (projection.pendingReversionReleases.length > 0) {
|
|
5578
|
+
const release = projection.pendingReversionReleases[0];
|
|
5579
|
+
if (this.reversionProviderProtocolVersion() !== release.protocolVersion) {
|
|
5580
|
+
throw new FlexHarnessValidationError(
|
|
5581
|
+
`Deleted session release requires protocolVersion ${release.protocolVersion}.`,
|
|
5582
|
+
);
|
|
5583
|
+
}
|
|
5584
|
+
try {
|
|
5585
|
+
await this.withReversionMaintenanceSignal((signal) =>
|
|
5586
|
+
this.turnReversionProvider!.release(Object.freeze({
|
|
5587
|
+
scopeId,
|
|
5588
|
+
scope,
|
|
5589
|
+
storageKey,
|
|
5590
|
+
sessionId,
|
|
5591
|
+
runId: release.runId,
|
|
5592
|
+
captureId: release.captureId,
|
|
5593
|
+
reference: cloneSerializable(release.reference),
|
|
5594
|
+
signal,
|
|
5595
|
+
})));
|
|
5596
|
+
} catch (error) {
|
|
5597
|
+
throw this.projectOperationError(
|
|
5598
|
+
error,
|
|
5599
|
+
'turnReversion',
|
|
5160
5600
|
scopeId,
|
|
5161
|
-
scope,
|
|
5162
|
-
storageKey,
|
|
5163
5601
|
sessionId,
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
'turnReversion',
|
|
5173
|
-
scopeId,
|
|
5174
|
-
sessionId,
|
|
5175
|
-
`release:${release.captureId}`,
|
|
5176
|
-
);
|
|
5602
|
+
`release:${release.captureId}`,
|
|
5603
|
+
);
|
|
5604
|
+
}
|
|
5605
|
+
await persistDirect({
|
|
5606
|
+
...projection,
|
|
5607
|
+
revision: projection.revision + 1,
|
|
5608
|
+
pendingReversionReleases: projection.pendingReversionReleases.slice(1),
|
|
5609
|
+
});
|
|
5177
5610
|
}
|
|
5178
5611
|
}
|
|
5179
5612
|
}
|
|
@@ -6248,8 +6681,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6248
6681
|
}
|
|
6249
6682
|
for (const tombstone of group) state.tombstones.delete(tombstone.sessionId);
|
|
6250
6683
|
scopeChanged = true;
|
|
6251
|
-
} catch {
|
|
6684
|
+
} catch (error) {
|
|
6252
6685
|
// A retained tombstone is retried by the next load or explicit delete call.
|
|
6686
|
+
if (state.lifecycle === 'fenced') {
|
|
6687
|
+
this.deferNamespaceDrain(state, error, { scopeId, scope });
|
|
6688
|
+
break;
|
|
6689
|
+
}
|
|
6253
6690
|
}
|
|
6254
6691
|
}
|
|
6255
6692
|
if (scopeChanged) {
|
|
@@ -6305,7 +6742,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6305
6742
|
revertCursor: 0,
|
|
6306
6743
|
excludedRunIds: [],
|
|
6307
6744
|
pendingReversionReleases: [],
|
|
6308
|
-
projectionSchemaVersion:
|
|
6745
|
+
projectionSchemaVersion: 3,
|
|
6309
6746
|
projectionReconciliationRequired: false,
|
|
6310
6747
|
projectionRevision: 0,
|
|
6311
6748
|
projectionQueue: Promise.resolve(),
|
|
@@ -6373,7 +6810,28 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6373
6810
|
throw combineErrors(acquisitionErrors);
|
|
6374
6811
|
}
|
|
6375
6812
|
const projection = projectionResult.value;
|
|
6376
|
-
const
|
|
6813
|
+
const legacyProjection = projection?.schemaVersion === 2 ? projection : undefined;
|
|
6814
|
+
const currentProjection = projection?.schemaVersion === 3 ? projection : undefined;
|
|
6815
|
+
const reversionSegments: IFlexReversionSegment[] = currentProjection
|
|
6816
|
+
? cloneSerializable(currentProjection.reversionSegments)
|
|
6817
|
+
: (legacyProjection?.reversionSegments ?? []).map((segment): IFlexReversionSegment => ({
|
|
6818
|
+
...cloneSerializable(segment),
|
|
6819
|
+
protocolVersion: 1,
|
|
6820
|
+
provenance: segment.workspaceCaptured ? 'workspace' : 'transcript',
|
|
6821
|
+
...(segment.workspaceCaptured
|
|
6822
|
+
? { disposition: segment.status === 'capturing' ? 'pending' : 'revertible' }
|
|
6823
|
+
: {}),
|
|
6824
|
+
}));
|
|
6825
|
+
const pendingReversion = currentProjection?.pendingReversion
|
|
6826
|
+
?? (legacyProjection?.pendingReversion?.kind === 'capture'
|
|
6827
|
+
? { ...legacyProjection.pendingReversion, protocolVersion: 1 as const }
|
|
6828
|
+
: legacyProjection?.pendingReversion);
|
|
6829
|
+
const pendingReversionReleases: IFlexPendingReversionRelease[] = currentProjection
|
|
6830
|
+
? cloneSerializable(currentProjection.pendingReversionReleases)
|
|
6831
|
+
: (legacyProjection?.pendingReversionReleases ?? []).map((release) => ({
|
|
6832
|
+
...cloneSerializable(release),
|
|
6833
|
+
protocolVersion: 1,
|
|
6834
|
+
}));
|
|
6377
6835
|
const permission = permissionResult.value;
|
|
6378
6836
|
const eventStore = eventStoreResult.value;
|
|
6379
6837
|
const jobStore = jobStoreResult.value;
|
|
@@ -6404,18 +6862,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6404
6862
|
session: cloneSerializable(metadata),
|
|
6405
6863
|
messages: cloneSerializable(projection?.messages ?? []),
|
|
6406
6864
|
stagedTerminals: cloneSerializable(projection?.stagedTerminals ?? []),
|
|
6407
|
-
reversionSegments
|
|
6408
|
-
revertCursor:
|
|
6409
|
-
excludedRunIds: cloneSerializable(
|
|
6410
|
-
|
|
6411
|
-
? {}
|
|
6412
|
-
: { pendingReversion: cloneSerializable(projectionV2.pendingReversion) }),
|
|
6413
|
-
pendingReversionReleases: cloneSerializable(
|
|
6414
|
-
projectionV2?.pendingReversionReleases ?? [],
|
|
6865
|
+
reversionSegments,
|
|
6866
|
+
revertCursor: currentProjection?.revertCursor ?? legacyProjection?.revertCursor ?? 0,
|
|
6867
|
+
excludedRunIds: cloneSerializable(
|
|
6868
|
+
currentProjection?.excludedRunIds ?? legacyProjection?.excludedRunIds ?? [],
|
|
6415
6869
|
),
|
|
6416
|
-
|
|
6870
|
+
...(pendingReversion === undefined
|
|
6871
|
+
? {}
|
|
6872
|
+
: { pendingReversion: cloneSerializable(pendingReversion) }),
|
|
6873
|
+
pendingReversionReleases,
|
|
6874
|
+
projectionSchemaVersion: projection?.schemaVersion ?? 3,
|
|
6417
6875
|
projectionReconciliationRequired: false,
|
|
6418
|
-
...(projection
|
|
6876
|
+
...(projection && projection.schemaVersion !== 3
|
|
6419
6877
|
? { projectionBaseline: cloneSerializable(projection) }
|
|
6420
6878
|
: {}),
|
|
6421
6879
|
projectionRevision: projection?.revision ?? 0,
|
|
@@ -6585,6 +7043,31 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6585
7043
|
'Capture-backed session recovery requires its turn reversion provider.',
|
|
6586
7044
|
);
|
|
6587
7045
|
}
|
|
7046
|
+
const requiredProtocols = new Set<number>([
|
|
7047
|
+
...stored.reversionSegments
|
|
7048
|
+
.filter((segment) => segment.captureId !== undefined)
|
|
7049
|
+
.map((segment) => segment.protocolVersion),
|
|
7050
|
+
...stored.pendingReversionReleases.map((release) => release.protocolVersion),
|
|
7051
|
+
...(stored.pendingReversion?.kind === 'capture'
|
|
7052
|
+
? [stored.pendingReversion.protocolVersion]
|
|
7053
|
+
: []),
|
|
7054
|
+
]);
|
|
7055
|
+
const providerProtocol = this.reversionProviderProtocolVersion();
|
|
7056
|
+
if (requiredProtocols.size > 0 && (
|
|
7057
|
+
requiredProtocols.size !== 1
|
|
7058
|
+
|| !requiredProtocols.has(providerProtocol ?? 0)
|
|
7059
|
+
)) {
|
|
7060
|
+
throw new FlexHarnessValidationError(
|
|
7061
|
+
`Capture-backed session recovery requires protocolVersion ${[...requiredProtocols].join(', ')}.`,
|
|
7062
|
+
);
|
|
7063
|
+
}
|
|
7064
|
+
if (stored.pendingReversion?.kind === 'capture' && stored.pendingReversionReleases.length > 0) {
|
|
7065
|
+
if (!state || scopeId === undefined || scope === undefined) {
|
|
7066
|
+
throw new FlexHarnessValidationError('Pending capture recovery is missing its scope context.');
|
|
7067
|
+
}
|
|
7068
|
+
await this.drainReversionReleases(state, stored, scopeId, scope, false);
|
|
7069
|
+
this.assertStateAcceptingWork(state);
|
|
7070
|
+
}
|
|
6588
7071
|
const outcomes = this.canonicalOutcomes(stored.agentSession.getEvents());
|
|
6589
7072
|
const firstHiddenCandidate = this.completedReversionCandidates(stored)[stored.revertCursor];
|
|
6590
7073
|
const reversionVisibilityBoundary = firstHiddenCandidate
|
|
@@ -6698,7 +7181,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6698
7181
|
snapshot,
|
|
6699
7182
|
expected,
|
|
6700
7183
|
);
|
|
6701
|
-
stored.projectionSchemaVersion =
|
|
7184
|
+
stored.projectionSchemaVersion = 3;
|
|
6702
7185
|
delete stored.projectionBaseline;
|
|
6703
7186
|
stored.projectionReconciliationRequired = false;
|
|
6704
7187
|
stored.projectionRevision = snapshot.revision;
|
|
@@ -6875,7 +7358,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6875
7358
|
: cloneSerializable(stored.pendingReversion);
|
|
6876
7359
|
const beforePendingReversionReleases = cloneSerializable(stored.pendingReversionReleases);
|
|
6877
7360
|
const beforeSnapshot: TFlexProjectionSnapshot = beforeBaseline ?? {
|
|
6878
|
-
schemaVersion:
|
|
7361
|
+
schemaVersion: 3,
|
|
6879
7362
|
revision: beforeRevision,
|
|
6880
7363
|
messages: cloneSerializable(beforeMessages),
|
|
6881
7364
|
stagedTerminals: cloneSerializable(beforeStages),
|
|
@@ -6917,7 +7400,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6917
7400
|
snapshot,
|
|
6918
7401
|
beforeRevision,
|
|
6919
7402
|
);
|
|
6920
|
-
stored.projectionSchemaVersion =
|
|
7403
|
+
stored.projectionSchemaVersion = 3;
|
|
6921
7404
|
delete stored.projectionBaseline;
|
|
6922
7405
|
stored.projectionReconciliationRequired = false;
|
|
6923
7406
|
stored.projectionRevision = snapshot.revision;
|
|
@@ -6928,7 +7411,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6928
7411
|
throw error;
|
|
6929
7412
|
}
|
|
6930
7413
|
if (error instanceof FlexHarnessStoreCommitUncertainError) {
|
|
6931
|
-
stored.projectionSchemaVersion =
|
|
7414
|
+
stored.projectionSchemaVersion = 3;
|
|
6932
7415
|
stored.projectionReconciliationRequired = true;
|
|
6933
7416
|
stored.projectionRevision = snapshot.revision;
|
|
6934
7417
|
state.lifecycle = 'fenced';
|
|
@@ -6945,7 +7428,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6945
7428
|
reconciliationError = loadError;
|
|
6946
7429
|
}
|
|
6947
7430
|
if (current && JSON.stringify(current) === JSON.stringify(snapshot)) {
|
|
6948
|
-
stored.projectionSchemaVersion =
|
|
7431
|
+
stored.projectionSchemaVersion = 3;
|
|
6949
7432
|
delete stored.projectionBaseline;
|
|
6950
7433
|
stored.projectionReconciliationRequired = false;
|
|
6951
7434
|
stored.projectionRevision = snapshot.revision;
|
|
@@ -6958,7 +7441,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6958
7441
|
restore();
|
|
6959
7442
|
throw error;
|
|
6960
7443
|
}
|
|
6961
|
-
stored.projectionSchemaVersion =
|
|
7444
|
+
stored.projectionSchemaVersion = 3;
|
|
6962
7445
|
stored.projectionReconciliationRequired = true;
|
|
6963
7446
|
stored.projectionRevision = snapshot.revision;
|
|
6964
7447
|
state.lifecycle = 'fenced';
|
|
@@ -7070,7 +7553,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7070
7553
|
revision: number,
|
|
7071
7554
|
): IFlexProjectionSnapshotCurrent {
|
|
7072
7555
|
return {
|
|
7073
|
-
schemaVersion:
|
|
7556
|
+
schemaVersion: 3,
|
|
7074
7557
|
revision,
|
|
7075
7558
|
messages: cloneSerializable(stored.messages),
|
|
7076
7559
|
stagedTerminals: cloneSerializable(stored.stagedTerminals),
|
|
@@ -7084,6 +7567,51 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7084
7567
|
};
|
|
7085
7568
|
}
|
|
7086
7569
|
|
|
7570
|
+
private upgradeProjectionSnapshot(
|
|
7571
|
+
projection: TFlexProjectionSnapshot,
|
|
7572
|
+
): IFlexProjectionSnapshotCurrent {
|
|
7573
|
+
if (projection.schemaVersion === 3) return cloneSerializable(projection);
|
|
7574
|
+
if (projection.schemaVersion === 1) {
|
|
7575
|
+
return {
|
|
7576
|
+
schemaVersion: 3,
|
|
7577
|
+
revision: projection.revision,
|
|
7578
|
+
messages: cloneSerializable(projection.messages),
|
|
7579
|
+
stagedTerminals: cloneSerializable(projection.stagedTerminals),
|
|
7580
|
+
reversionSegments: [],
|
|
7581
|
+
revertCursor: 0,
|
|
7582
|
+
excludedRunIds: [],
|
|
7583
|
+
pendingReversionReleases: [],
|
|
7584
|
+
};
|
|
7585
|
+
}
|
|
7586
|
+
return {
|
|
7587
|
+
schemaVersion: 3,
|
|
7588
|
+
revision: projection.revision,
|
|
7589
|
+
messages: cloneSerializable(projection.messages),
|
|
7590
|
+
stagedTerminals: cloneSerializable(projection.stagedTerminals),
|
|
7591
|
+
reversionSegments: projection.reversionSegments.map((segment) => ({
|
|
7592
|
+
...cloneSerializable(segment),
|
|
7593
|
+
protocolVersion: 1,
|
|
7594
|
+
provenance: segment.workspaceCaptured ? 'workspace' as const : 'transcript' as const,
|
|
7595
|
+
...(segment.workspaceCaptured
|
|
7596
|
+
? { disposition: segment.status === 'capturing' ? 'pending' as const : 'revertible' as const }
|
|
7597
|
+
: {}),
|
|
7598
|
+
})),
|
|
7599
|
+
revertCursor: projection.revertCursor,
|
|
7600
|
+
excludedRunIds: cloneSerializable(projection.excludedRunIds),
|
|
7601
|
+
...(projection.pendingReversion === undefined
|
|
7602
|
+
? {}
|
|
7603
|
+
: {
|
|
7604
|
+
pendingReversion: projection.pendingReversion.kind === 'capture'
|
|
7605
|
+
? { ...cloneSerializable(projection.pendingReversion), protocolVersion: 1 }
|
|
7606
|
+
: cloneSerializable(projection.pendingReversion),
|
|
7607
|
+
}),
|
|
7608
|
+
pendingReversionReleases: projection.pendingReversionReleases.map((release) => ({
|
|
7609
|
+
...cloneSerializable(release),
|
|
7610
|
+
protocolVersion: 1,
|
|
7611
|
+
})),
|
|
7612
|
+
};
|
|
7613
|
+
}
|
|
7614
|
+
|
|
7087
7615
|
private async reconcileProjectionFromStore(stored: IStoredSessionState): Promise<void> {
|
|
7088
7616
|
if (!stored.projectionReconciliationRequired) return;
|
|
7089
7617
|
const projection = await this.stores.projections.load(
|
|
@@ -7093,24 +7621,17 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7093
7621
|
if (!projection) {
|
|
7094
7622
|
throw new FlexHarnessValidationError('Uncertain projection persistence could not be reloaded.');
|
|
7095
7623
|
}
|
|
7096
|
-
|
|
7097
|
-
stored.
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
stored.reversionSegments = [];
|
|
7108
|
-
stored.revertCursor = 0;
|
|
7109
|
-
stored.excludedRunIds = [];
|
|
7110
|
-
delete stored.pendingReversion;
|
|
7111
|
-
stored.pendingReversionReleases = [];
|
|
7112
|
-
stored.projectionBaseline = cloneSerializable(projection);
|
|
7113
|
-
}
|
|
7624
|
+
const current = this.upgradeProjectionSnapshot(projection);
|
|
7625
|
+
stored.messages = current.messages;
|
|
7626
|
+
stored.stagedTerminals = current.stagedTerminals;
|
|
7627
|
+
stored.reversionSegments = current.reversionSegments;
|
|
7628
|
+
stored.revertCursor = current.revertCursor;
|
|
7629
|
+
stored.excludedRunIds = current.excludedRunIds;
|
|
7630
|
+
if (current.pendingReversion === undefined) delete stored.pendingReversion;
|
|
7631
|
+
else stored.pendingReversion = current.pendingReversion;
|
|
7632
|
+
stored.pendingReversionReleases = current.pendingReversionReleases;
|
|
7633
|
+
if (projection.schemaVersion === 3) delete stored.projectionBaseline;
|
|
7634
|
+
else stored.projectionBaseline = cloneSerializable(projection);
|
|
7114
7635
|
stored.projectionSchemaVersion = projection.schemaVersion;
|
|
7115
7636
|
stored.projectionRevision = projection.revision;
|
|
7116
7637
|
stored.projectionReconciliationRequired = false;
|
|
@@ -7807,12 +8328,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7807
8328
|
const scope = await this.scopeResolver.resolveScope(scopeId);
|
|
7808
8329
|
this.assertOpen();
|
|
7809
8330
|
validateIdentifier(scope.storageKey, 'resolved storageKey');
|
|
7810
|
-
|
|
7811
|
-
if (invocationOwner?.storageKey === scope.storageKey) {
|
|
7812
|
-
throw this.trustInternalError(
|
|
7813
|
-
new FlexHarnessSlashCommandReentryError(invocationOwner.sessionId),
|
|
7814
|
-
);
|
|
7815
|
-
}
|
|
8331
|
+
this.assertNoAmbientSlashCommandTeardown({ storageKey: scope.storageKey });
|
|
7816
8332
|
const errors: unknown[] = [];
|
|
7817
8333
|
const listingSettlement = this.abortSlashCommandListings(
|
|
7818
8334
|
scope.storageKey,
|
|
@@ -7895,8 +8411,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7895
8411
|
return;
|
|
7896
8412
|
}
|
|
7897
8413
|
if (state.lifecycle !== 'fenced') state.lifecycle = 'retiring';
|
|
7898
|
-
const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
|
|
7899
8414
|
const errors: unknown[] = [];
|
|
8415
|
+
const listingSettlement = this.abortSlashCommandListings(storageKey, reason);
|
|
8416
|
+
if (listingSettlement) await listingSettlement;
|
|
8417
|
+
await this.abortSlashCommandExecutions(storageKey, reason, errors);
|
|
8418
|
+
const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
|
|
7900
8419
|
const contextFor = (sessionId: string) => invocation
|
|
7901
8420
|
? this.createCompactorContext(invocation.scopeId, invocation.scope, storageKey, sessionId)
|
|
7902
8421
|
: undefined;
|
|
@@ -8061,6 +8580,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8061
8580
|
if (errors.length > 0) throw combineErrors(errors);
|
|
8062
8581
|
this.compactorInvocationContext.disable();
|
|
8063
8582
|
this.slashCommandInvocationContext.disable();
|
|
8583
|
+
this.slashCommandActivityContext.disable();
|
|
8064
8584
|
this.stateLoads.clear();
|
|
8065
8585
|
this.scopeAdmissions.clear();
|
|
8066
8586
|
this.scopeRetirements.clear();
|
|
@@ -8171,6 +8691,23 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8171
8691
|
}
|
|
8172
8692
|
const state = await stateLoad;
|
|
8173
8693
|
this.assertOpen();
|
|
8694
|
+
if (state.lifecycle === 'fenced') {
|
|
8695
|
+
const drain = this.storageDrains.get(scope.storageKey) ?? this.drainStorage(
|
|
8696
|
+
scope.storageKey,
|
|
8697
|
+
stateLoad,
|
|
8698
|
+
this.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.')),
|
|
8699
|
+
{ scopeId, scope: scope.scope },
|
|
8700
|
+
);
|
|
8701
|
+
if (this.ambientSlashCommandOwner({ storageKey: scope.storageKey })) {
|
|
8702
|
+
throw this.createScopeRetirementError();
|
|
8703
|
+
}
|
|
8704
|
+
try {
|
|
8705
|
+
await drain;
|
|
8706
|
+
} catch {
|
|
8707
|
+
// Fenced cleanup ownership remains available to explicit retirement or disposal.
|
|
8708
|
+
}
|
|
8709
|
+
throw this.createScopeRetirementError();
|
|
8710
|
+
}
|
|
8174
8711
|
if (
|
|
8175
8712
|
admission.retiring
|
|
8176
8713
|
|| admission.generation !== generation
|
|
@@ -8205,6 +8742,29 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8205
8742
|
return this.trustInternalError(new FlexHarnessAbortError(scopeRetirementMessage));
|
|
8206
8743
|
}
|
|
8207
8744
|
|
|
8745
|
+
private ambientSlashCommandOwner(
|
|
8746
|
+
criteria: { scopeId?: string; storageKey?: string; sessionId?: string } = {},
|
|
8747
|
+
): ISlashCommandInvocationOwner | ISlashCommandActivityOwner | undefined {
|
|
8748
|
+
const matches = (owner: ISlashCommandInvocationOwner | ISlashCommandActivityOwner) => (
|
|
8749
|
+
(criteria.scopeId === undefined || owner.scopeId === criteria.scopeId)
|
|
8750
|
+
&& (criteria.storageKey === undefined || owner.storageKey === criteria.storageKey)
|
|
8751
|
+
&& (criteria.sessionId === undefined || owner.sessionId === criteria.sessionId)
|
|
8752
|
+
);
|
|
8753
|
+
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
8754
|
+
if (invocationOwner && matches(invocationOwner)) return invocationOwner;
|
|
8755
|
+
const activityOwner = this.slashCommandActivityContext.getStore();
|
|
8756
|
+
return activityOwner && matches(activityOwner) ? activityOwner : undefined;
|
|
8757
|
+
}
|
|
8758
|
+
|
|
8759
|
+
private assertNoAmbientSlashCommandTeardown(
|
|
8760
|
+
criteria: { scopeId?: string; storageKey?: string; sessionId?: string } = {},
|
|
8761
|
+
): void {
|
|
8762
|
+
const owner = this.ambientSlashCommandOwner(criteria);
|
|
8763
|
+
if (owner) {
|
|
8764
|
+
throw this.trustInternalError(new FlexHarnessSlashCommandReentryError(owner.sessionId));
|
|
8765
|
+
}
|
|
8766
|
+
}
|
|
8767
|
+
|
|
8208
8768
|
private assertStateAcceptingWork(state: IStorageState): void {
|
|
8209
8769
|
if (state.lifecycle !== 'active' || this.storageDrains.has(state.storageKey)) {
|
|
8210
8770
|
throw this.createScopeRetirementError();
|
|
@@ -8259,8 +8819,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8259
8819
|
}
|
|
8260
8820
|
|
|
8261
8821
|
private visibleMessages(stored: IStoredSessionState): IFlexMessage[] {
|
|
8262
|
-
const
|
|
8263
|
-
const firstHiddenSegment = firstHiddenUnit?.segments[0];
|
|
8822
|
+
const firstHiddenSegment = this.reversionGroups(stored)[stored.revertCursor]?.segments[0];
|
|
8264
8823
|
if (!firstHiddenSegment) return stored.messages;
|
|
8265
8824
|
const boundary = stored.messages.findIndex((message) => message.runId === firstHiddenSegment.runId);
|
|
8266
8825
|
return boundary < 0 ? stored.messages : stored.messages.slice(0, boundary);
|