@modelprofile.com/flexharness 3.7.0 → 4.0.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 +21 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +65 -2
- package/dist_ts/classes.flexharness.js +1401 -222
- package/dist_ts/classes.stores.d.ts +10 -1
- package/dist_ts/classes.stores.js +164 -2
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.d.ts +184 -6
- package/dist_ts/interfaces.js +16 -1
- package/dist_ts/utils.json.d.ts +3 -2
- package/dist_ts/utils.json.js +278 -12
- package/dist_ts/utils.projectmanagement.d.ts +5 -0
- package/dist_ts/utils.projectmanagement.js +141 -0
- package/dist_ts_migration/v3_legacyflexharness.js +1 -1
- package/package.json +1 -1
- package/readme.hints.md +7 -4
- package/readme.md +207 -25
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +2024 -255
- package/ts/classes.stores.ts +265 -6
- package/ts/index.ts +6 -0
- package/ts/interfaces.ts +254 -6
- package/ts/utils.json.ts +321 -13
- package/ts/utils.projectmanagement.ts +226 -0
- package/ts_migration/v3_legacyflexharness.ts +1 -1
|
@@ -18,16 +18,23 @@ import {
|
|
|
18
18
|
errorToInfo,
|
|
19
19
|
} from './errors.js';
|
|
20
20
|
import {
|
|
21
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS,
|
|
22
|
+
FLEX_REVERSION_MAX_AFFECTED_WORKSPACES,
|
|
21
23
|
FLEX_REVERSION_DEFAULT_LIMITS,
|
|
22
24
|
FLEX_REVERSION_MAXIMUM_LIMITS,
|
|
25
|
+
FLEX_REVERSION_REASON_CODE_MAX_BYTES,
|
|
23
26
|
FLEX_REVERSION_REFERENCE_MAX_BYTES,
|
|
27
|
+
FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES,
|
|
28
|
+
FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES,
|
|
24
29
|
} from './interfaces.js';
|
|
25
30
|
import type {
|
|
26
31
|
IFlexAgentContextInvocation,
|
|
27
32
|
IFlexAgentSessionPolicy,
|
|
33
|
+
IFlexAffectedWorkspace,
|
|
28
34
|
IFlexAttachmentMessagePart,
|
|
29
35
|
IFlexBackgroundExecution,
|
|
30
36
|
IFlexCallbackLimits,
|
|
37
|
+
IFlexCreateProjectTaskInput,
|
|
31
38
|
IFlexCreateSessionOptions,
|
|
32
39
|
IFlexErrorInfo,
|
|
33
40
|
IFlexEventArchiveMetadata,
|
|
@@ -42,6 +49,15 @@ import type {
|
|
|
42
49
|
IFlexPermissionRequest,
|
|
43
50
|
IFlexPermissionRequestInput,
|
|
44
51
|
IFlexPermissionSnapshot,
|
|
52
|
+
IFlexProjectGoalResult,
|
|
53
|
+
IFlexProjectManagementSnapshot,
|
|
54
|
+
IFlexProjectManagementTombstone,
|
|
55
|
+
IFlexProjectManagementWriteContext,
|
|
56
|
+
IFlexProjectScratchpadResult,
|
|
57
|
+
IFlexProjectStateResult,
|
|
58
|
+
IFlexProjectTask,
|
|
59
|
+
IFlexProjectTaskResult,
|
|
60
|
+
IFlexProjectTasksResult,
|
|
45
61
|
IFlexPromptAdmission,
|
|
46
62
|
IFlexPromptQueueAdmission,
|
|
47
63
|
IFlexPromptQueueEntry,
|
|
@@ -61,6 +77,8 @@ import type {
|
|
|
61
77
|
IFlexSchedulePromptOptions,
|
|
62
78
|
IFlexScopeSnapshot,
|
|
63
79
|
IFlexSession,
|
|
80
|
+
IFlexSessionReversionGroup,
|
|
81
|
+
IFlexSessionReversionInfo,
|
|
64
82
|
IFlexSessionTombstone,
|
|
65
83
|
IFlexSlashCommandDescriptor,
|
|
66
84
|
IFlexSlashCommandExecutionOptions,
|
|
@@ -74,6 +92,7 @@ import type {
|
|
|
74
92
|
IFlexUncertainToolExecution,
|
|
75
93
|
IFlexUndoSessionResult,
|
|
76
94
|
IFlexUpdateSessionOptions,
|
|
95
|
+
IFlexUpdateProjectTaskInput,
|
|
77
96
|
IFlexUsage,
|
|
78
97
|
IJsonObject,
|
|
79
98
|
TFlexAgentRunResult,
|
|
@@ -86,6 +105,8 @@ import type {
|
|
|
86
105
|
TFlexHarnessEventListener,
|
|
87
106
|
TFlexMessagePart,
|
|
88
107
|
TFlexPermissionDecision,
|
|
108
|
+
TFlexProjectTaskPriority,
|
|
109
|
+
TFlexProjectTaskStatus,
|
|
89
110
|
TFlexPrompt,
|
|
90
111
|
TFlexPromptPart,
|
|
91
112
|
TFlexPromptQueueStatus,
|
|
@@ -93,6 +114,9 @@ import type {
|
|
|
93
114
|
TFlexSlashCommandRegistration,
|
|
94
115
|
TFlexPendingReversion,
|
|
95
116
|
TFlexProjectionSnapshot,
|
|
117
|
+
TFlexReversionPolicy,
|
|
118
|
+
TFlexSessionReversionGroupKind,
|
|
119
|
+
TFlexTurnReversionFinalizedOutcome,
|
|
96
120
|
TFlexToolExecutionReconciliation,
|
|
97
121
|
TJsonValue,
|
|
98
122
|
} from './interfaces.js';
|
|
@@ -104,6 +128,11 @@ import {
|
|
|
104
128
|
normalizeJsonValue,
|
|
105
129
|
resolveJsonLimits,
|
|
106
130
|
} from './utils.json.js';
|
|
131
|
+
import {
|
|
132
|
+
assertFlexProjectManagementRecord,
|
|
133
|
+
assertFlexProjectManagementSnapshot,
|
|
134
|
+
createEmptyFlexProjectManagementSnapshot,
|
|
135
|
+
} from './utils.projectmanagement.js';
|
|
107
136
|
import {
|
|
108
137
|
hydrateAgentMessages,
|
|
109
138
|
normalizeFlexPrompt,
|
|
@@ -138,7 +167,7 @@ interface IStoredSessionState {
|
|
|
138
167
|
excludedRunIds: string[];
|
|
139
168
|
pendingReversion?: TFlexPendingReversion;
|
|
140
169
|
pendingReversionReleases: IFlexPendingReversionRelease[];
|
|
141
|
-
projectionSchemaVersion: 1 | 2;
|
|
170
|
+
projectionSchemaVersion: 1 | 2 | 3;
|
|
142
171
|
projectionBaseline?: TFlexProjectionSnapshot;
|
|
143
172
|
projectionReconciliationRequired: boolean;
|
|
144
173
|
projectionRevision: number;
|
|
@@ -168,6 +197,18 @@ interface IRetainedSessionCleanup {
|
|
|
168
197
|
domainsCompleted: boolean;
|
|
169
198
|
}
|
|
170
199
|
|
|
200
|
+
interface IReversionGroup {
|
|
201
|
+
target: IFlexReversionSegment;
|
|
202
|
+
segments: IFlexReversionSegment[];
|
|
203
|
+
kind: TFlexSessionReversionGroupKind;
|
|
204
|
+
affectedWorkspaces: IFlexAffectedWorkspace[];
|
|
205
|
+
affectedWorkspacesTruncated: boolean;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
type TNormalizedCaptureInspection =
|
|
209
|
+
| { status: 'missing' | 'prepared' | 'unknown' }
|
|
210
|
+
| { status: 'finalized'; outcome: TFlexTurnReversionFinalizedOutcome };
|
|
211
|
+
|
|
171
212
|
interface IDetachedCleanup {
|
|
172
213
|
cleanup: () => Promise<void> | void;
|
|
173
214
|
projectError: (error: unknown) => Error;
|
|
@@ -332,6 +373,12 @@ interface ISlashCommandInvocationOwner {
|
|
|
332
373
|
sessionKey: string;
|
|
333
374
|
}
|
|
334
375
|
|
|
376
|
+
interface ISlashCommandActivityOwner {
|
|
377
|
+
scopeId: string;
|
|
378
|
+
storageKey: string;
|
|
379
|
+
sessionId: string;
|
|
380
|
+
}
|
|
381
|
+
|
|
335
382
|
interface IRunResultProjection {
|
|
336
383
|
text: string;
|
|
337
384
|
steps: number;
|
|
@@ -369,6 +416,47 @@ interface IFlexSubagentAcquisition {
|
|
|
369
416
|
created: boolean;
|
|
370
417
|
}
|
|
371
418
|
|
|
419
|
+
interface INormalizedProjectManagementTools {
|
|
420
|
+
readonly task: boolean;
|
|
421
|
+
readonly goal: boolean;
|
|
422
|
+
readonly scratchpad: boolean;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
interface INormalizedBuiltInTools {
|
|
426
|
+
readonly renameSession: boolean;
|
|
427
|
+
readonly projectManagement?: INormalizedProjectManagementTools;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
type TProjectTaskToolInput =
|
|
431
|
+
| { action: 'list' }
|
|
432
|
+
| {
|
|
433
|
+
action: 'create';
|
|
434
|
+
id?: string;
|
|
435
|
+
content: string;
|
|
436
|
+
status?: TFlexProjectTaskStatus;
|
|
437
|
+
priority?: TFlexProjectTaskPriority;
|
|
438
|
+
}
|
|
439
|
+
| {
|
|
440
|
+
action: 'update';
|
|
441
|
+
id: string;
|
|
442
|
+
content?: string;
|
|
443
|
+
status?: TFlexProjectTaskStatus;
|
|
444
|
+
priority?: TFlexProjectTaskPriority;
|
|
445
|
+
}
|
|
446
|
+
| { action: 'delete'; id: string }
|
|
447
|
+
| { action: 'clear' };
|
|
448
|
+
|
|
449
|
+
type TProjectGoalToolInput =
|
|
450
|
+
| { action: 'get' }
|
|
451
|
+
| { action: 'set'; goal: string }
|
|
452
|
+
| { action: 'clear' };
|
|
453
|
+
|
|
454
|
+
type TProjectScratchpadToolInput =
|
|
455
|
+
| { action: 'get' }
|
|
456
|
+
| { action: 'set'; content: string }
|
|
457
|
+
| { action: 'append'; content: string }
|
|
458
|
+
| { action: 'clear' };
|
|
459
|
+
|
|
372
460
|
type TEventDetails = Record<string, unknown> & {
|
|
373
461
|
type: TFlexHarnessEvent['type'];
|
|
374
462
|
};
|
|
@@ -416,6 +504,7 @@ const maxResourceToolNameBytes = 512;
|
|
|
416
504
|
const maxSlashCommandRegistrations = 128;
|
|
417
505
|
const maxSlashCommandDescriptionBytes = 2048;
|
|
418
506
|
const maxSlashCommandTemplateBytes = FLEX_SLASH_COMMAND_MAX_INPUT_BYTES;
|
|
507
|
+
const maxProjectManagementTombstoneConflicts = 8;
|
|
419
508
|
const resourceToolStemLength = 16;
|
|
420
509
|
const repairCancellationMessage = 'The process stopped before this run completed.';
|
|
421
510
|
const scopeRetirementMessage = 'The scope is being retired.';
|
|
@@ -427,6 +516,43 @@ const externalErrorFallback: IFlexErrorInfo = Object.freeze({
|
|
|
427
516
|
code: 'FLEX_EXTERNAL_ERROR',
|
|
428
517
|
});
|
|
429
518
|
|
|
519
|
+
function createSessionGenerationId(): string {
|
|
520
|
+
return `generation_${plugins.crypto.randomUUID()}`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function deriveLegacySessionGenerationId(
|
|
524
|
+
storageKey: string,
|
|
525
|
+
sessionId: string,
|
|
526
|
+
createdAt: string,
|
|
527
|
+
): string {
|
|
528
|
+
const digest = plugins.crypto.createHash('sha256').update(JSON.stringify([
|
|
529
|
+
'flexharness-legacy-session-generation-v1',
|
|
530
|
+
storageKey,
|
|
531
|
+
sessionId,
|
|
532
|
+
createdAt,
|
|
533
|
+
])).digest('hex');
|
|
534
|
+
return `legacy_${digest}`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function requireSessionGeneration(
|
|
538
|
+
session: Pick<
|
|
539
|
+
IFlexSession | IFlexSessionTombstone,
|
|
540
|
+
'sessionGenerationId' | 'sessionGenerationSequence'
|
|
541
|
+
>,
|
|
542
|
+
): { sessionGenerationId: string; sessionGenerationSequence: number } {
|
|
543
|
+
if (
|
|
544
|
+
!session.sessionGenerationId
|
|
545
|
+
|| !Number.isSafeInteger(session.sessionGenerationSequence)
|
|
546
|
+
|| Number(session.sessionGenerationSequence) < 1
|
|
547
|
+
) {
|
|
548
|
+
throw new FlexHarnessValidationError('Session generation metadata is missing.');
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
sessionGenerationId: session.sessionGenerationId,
|
|
552
|
+
sessionGenerationSequence: session.sessionGenerationSequence as number,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
430
556
|
function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
|
|
431
557
|
return Boolean(
|
|
432
558
|
value
|
|
@@ -635,6 +761,58 @@ function normalizeSubagents(
|
|
|
635
761
|
return Object.freeze(normalized);
|
|
636
762
|
}
|
|
637
763
|
|
|
764
|
+
function normalizeBuiltInTools(
|
|
765
|
+
options: IFlexHarnessOptions<unknown>['builtInTools'],
|
|
766
|
+
): INormalizedBuiltInTools {
|
|
767
|
+
if (options === undefined) return Object.freeze({ renameSession: false });
|
|
768
|
+
if (
|
|
769
|
+
!options
|
|
770
|
+
|| typeof options !== 'object'
|
|
771
|
+
|| Array.isArray(options)
|
|
772
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(options))
|
|
773
|
+
) {
|
|
774
|
+
throw new FlexHarnessValidationError('builtInTools must be a plain object.');
|
|
775
|
+
}
|
|
776
|
+
const unsupported = Object.keys(options)
|
|
777
|
+
.find((key) => !['renameSession', 'projectManagement'].includes(key));
|
|
778
|
+
if (unsupported) throw new FlexHarnessValidationError(`builtInTools does not support "${unsupported}".`);
|
|
779
|
+
if (options.renameSession !== undefined && typeof options.renameSession !== 'boolean') {
|
|
780
|
+
throw new FlexHarnessValidationError('builtInTools.renameSession must be a boolean.');
|
|
781
|
+
}
|
|
782
|
+
const projectManagement = options.projectManagement;
|
|
783
|
+
if (projectManagement === undefined) {
|
|
784
|
+
return Object.freeze({ renameSession: options.renameSession ?? false });
|
|
785
|
+
}
|
|
786
|
+
if (
|
|
787
|
+
!projectManagement
|
|
788
|
+
|| typeof projectManagement !== 'object'
|
|
789
|
+
|| Array.isArray(projectManagement)
|
|
790
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(projectManagement))
|
|
791
|
+
) {
|
|
792
|
+
throw new FlexHarnessValidationError('builtInTools.projectManagement must be a plain object.');
|
|
793
|
+
}
|
|
794
|
+
const unsupportedProjectOption = Object.keys(projectManagement)
|
|
795
|
+
.find((key) => !['task', 'goal', 'scratchpad'].includes(key));
|
|
796
|
+
if (unsupportedProjectOption) {
|
|
797
|
+
throw new FlexHarnessValidationError(
|
|
798
|
+
`builtInTools.projectManagement does not support "${unsupportedProjectOption}".`,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
for (const key of ['task', 'goal', 'scratchpad'] as const) {
|
|
802
|
+
if (projectManagement[key] !== undefined && typeof projectManagement[key] !== 'boolean') {
|
|
803
|
+
throw new FlexHarnessValidationError(`builtInTools.projectManagement.${key} must be a boolean.`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return Object.freeze({
|
|
807
|
+
renameSession: options.renameSession ?? false,
|
|
808
|
+
projectManagement: Object.freeze({
|
|
809
|
+
task: projectManagement.task ?? true,
|
|
810
|
+
goal: projectManagement.goal ?? true,
|
|
811
|
+
scratchpad: projectManagement.scratchpad ?? true,
|
|
812
|
+
}),
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
638
816
|
function normalizeSlashCommands<TScope>(
|
|
639
817
|
registrations: readonly TFlexSlashCommandRegistration<TScope>[] | undefined,
|
|
640
818
|
): ReadonlyMap<string, TRegisteredSlashCommand<TScope>> {
|
|
@@ -969,13 +1147,81 @@ function validateUpdateSessionOptions(options: IFlexUpdateSessionOptions): void
|
|
|
969
1147
|
const unsupportedKey = keys.find((key) => key !== 'title' && key !== 'archived');
|
|
970
1148
|
if (unsupportedKey) throw new FlexHarnessValidationError(`updateSession does not support "${unsupportedKey}".`);
|
|
971
1149
|
if (Object.prototype.hasOwnProperty.call(options, 'title') && options.title !== null) {
|
|
972
|
-
|
|
1150
|
+
validateUtf8String(
|
|
1151
|
+
options.title,
|
|
1152
|
+
'title',
|
|
1153
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTitleBytes,
|
|
1154
|
+
true,
|
|
1155
|
+
);
|
|
973
1156
|
}
|
|
974
1157
|
if (Object.prototype.hasOwnProperty.call(options, 'archived') && typeof options.archived !== 'boolean') {
|
|
975
1158
|
throw new FlexHarnessValidationError('archived must be a boolean.');
|
|
976
1159
|
}
|
|
977
1160
|
}
|
|
978
1161
|
|
|
1162
|
+
function validateProjectTaskStatus(value: unknown): asserts value is TFlexProjectTaskStatus {
|
|
1163
|
+
if (!['pending', 'in_progress', 'completed', 'cancelled'].includes(String(value))) {
|
|
1164
|
+
throw new FlexHarnessValidationError('Project task status is invalid.');
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function validateProjectTaskPriority(value: unknown): asserts value is TFlexProjectTaskPriority {
|
|
1169
|
+
if (!['high', 'medium', 'low'].includes(String(value))) {
|
|
1170
|
+
throw new FlexHarnessValidationError('Project task priority is invalid.');
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function validateProjectTaskId(value: unknown): asserts value is string {
|
|
1175
|
+
validateUtf8String(
|
|
1176
|
+
value,
|
|
1177
|
+
'project task id',
|
|
1178
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskIdBytes,
|
|
1179
|
+
true,
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function validateProjectTaskContent(value: unknown): asserts value is string {
|
|
1184
|
+
validateUtf8String(
|
|
1185
|
+
value,
|
|
1186
|
+
'project task content',
|
|
1187
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskContentBytes,
|
|
1188
|
+
true,
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function validateCreateProjectTaskInput(input: IFlexCreateProjectTaskInput): void {
|
|
1193
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
1194
|
+
throw new FlexHarnessValidationError('Project task create input must be a plain object.');
|
|
1195
|
+
}
|
|
1196
|
+
const unsupported = Object.keys(input)
|
|
1197
|
+
.find((key) => !['id', 'content', 'status', 'priority'].includes(key));
|
|
1198
|
+
if (unsupported) {
|
|
1199
|
+
throw new FlexHarnessValidationError(`Project task create input does not support "${unsupported}".`);
|
|
1200
|
+
}
|
|
1201
|
+
validateProjectTaskId(input.id);
|
|
1202
|
+
validateProjectTaskContent(input.content);
|
|
1203
|
+
if (input.status !== undefined) validateProjectTaskStatus(input.status);
|
|
1204
|
+
if (input.priority !== undefined) validateProjectTaskPriority(input.priority);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function validateUpdateProjectTaskInput(input: IFlexUpdateProjectTaskInput): void {
|
|
1208
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
1209
|
+
throw new FlexHarnessValidationError('Project task update input must be a plain object.');
|
|
1210
|
+
}
|
|
1211
|
+
const keys = Object.keys(input);
|
|
1212
|
+
const unsupported = keys.find((key) => !['id', 'content', 'status', 'priority'].includes(key));
|
|
1213
|
+
if (unsupported) {
|
|
1214
|
+
throw new FlexHarnessValidationError(`Project task update input does not support "${unsupported}".`);
|
|
1215
|
+
}
|
|
1216
|
+
if (!keys.some((key) => key !== 'id')) {
|
|
1217
|
+
throw new FlexHarnessValidationError('Project task update requires a changed field.');
|
|
1218
|
+
}
|
|
1219
|
+
validateProjectTaskId(input.id);
|
|
1220
|
+
if (input.content !== undefined) validateProjectTaskContent(input.content);
|
|
1221
|
+
if (input.status !== undefined) validateProjectTaskStatus(input.status);
|
|
1222
|
+
if (input.priority !== undefined) validateProjectTaskPriority(input.priority);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
979
1225
|
function validatePromptOptions(options: IFlexPromptOptions, scheduled: boolean): void {
|
|
980
1226
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
981
1227
|
throw new FlexHarnessValidationError('Prompt options must be a plain object.');
|
|
@@ -1100,6 +1346,22 @@ function normalizeAgentSessionPolicy<TScope>(
|
|
|
1100
1346
|
};
|
|
1101
1347
|
}
|
|
1102
1348
|
|
|
1349
|
+
function requireHarnessStores(stores: IFlexHarnessStores): IFlexHarnessStores {
|
|
1350
|
+
const projectManagement = stores?.projectManagement;
|
|
1351
|
+
if (
|
|
1352
|
+
!projectManagement
|
|
1353
|
+
|| typeof projectManagement.load !== 'function'
|
|
1354
|
+
|| typeof projectManagement.save !== 'function'
|
|
1355
|
+
|| typeof projectManagement.tombstoneSession !== 'function'
|
|
1356
|
+
|| typeof projectManagement.purgeNamespace !== 'function'
|
|
1357
|
+
) {
|
|
1358
|
+
throw new FlexHarnessValidationError(
|
|
1359
|
+
'stores.projectManagement requires load/save/tombstoneSession/purgeNamespace methods.',
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
return stores;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1103
1365
|
export class FlexHarness<TScope = unknown> {
|
|
1104
1366
|
private readonly scopeResolver: IFlexHarnessOptions<TScope>['scopeResolver'];
|
|
1105
1367
|
private readonly modelResolver: IFlexHarnessOptions<TScope>['modelResolver'];
|
|
@@ -1113,8 +1375,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1113
1375
|
private readonly promptQueueLimits: Required<IFlexPromptQueueLimits>;
|
|
1114
1376
|
private readonly reversionLimits: Required<IFlexReversionLimits>;
|
|
1115
1377
|
private readonly turnReversionProvider: IFlexHarnessOptions<TScope>['turnReversionProvider'];
|
|
1378
|
+
private readonly reversionPolicy: TFlexReversionPolicy;
|
|
1116
1379
|
private readonly externalErrorProjector?: TFlexExternalErrorProjector;
|
|
1117
1380
|
private readonly subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
|
|
1381
|
+
private readonly builtInTools: INormalizedBuiltInTools;
|
|
1118
1382
|
private readonly slashCommands: ReadonlyMap<string, TRegisteredSlashCommand<TScope>>;
|
|
1119
1383
|
private readonly maxSubagentDepth: number;
|
|
1120
1384
|
private readonly maxSubagentCallsPerRun: number;
|
|
@@ -1135,6 +1399,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1135
1399
|
private readonly orphanedTombstoneCleanups = new Map<string, IOrphanedTombstoneCleanup>();
|
|
1136
1400
|
private readonly orphanedTombstoneOwners = new Map<string, IOrphanedTombstoneOwner<TScope>>();
|
|
1137
1401
|
private readonly pendingPromptAdmissionOwners = new Set<IPendingPromptAdmission>();
|
|
1402
|
+
private readonly projectManagementQueues = new Map<string, Promise<void>>();
|
|
1138
1403
|
private readonly activeSlashCommandExecutions = new Map<string, IActiveSlashCommandExecution>();
|
|
1139
1404
|
private readonly activeSlashCommandListings = new Set<IActiveSlashCommandListing>();
|
|
1140
1405
|
private readonly compactorInvocationContext = new plugins.AsyncLocalStorage<
|
|
@@ -1143,6 +1408,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1143
1408
|
private readonly slashCommandInvocationContext = new plugins.AsyncLocalStorage<
|
|
1144
1409
|
ISlashCommandInvocationOwner
|
|
1145
1410
|
>();
|
|
1411
|
+
private readonly slashCommandActivityContext = new plugins.AsyncLocalStorage<
|
|
1412
|
+
ISlashCommandActivityOwner
|
|
1413
|
+
>();
|
|
1146
1414
|
private readonly deferredCompactorContexts = new WeakMap<
|
|
1147
1415
|
IFlexAgentContextInvocation<unknown>,
|
|
1148
1416
|
IFlexAgentContextInvocation<unknown>
|
|
@@ -1163,15 +1431,34 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1163
1431
|
this.toolProvider = options.toolProvider;
|
|
1164
1432
|
this.resourceToolProviderResolver = options.resourceToolProviderResolver;
|
|
1165
1433
|
this.executionContextProvider = options.executionContextProvider;
|
|
1166
|
-
this.stores = options.stores ?? new InMemoryFlexHarnessStores();
|
|
1434
|
+
this.stores = requireHarnessStores(options.stores ?? new InMemoryFlexHarnessStores());
|
|
1167
1435
|
this.agentSessionPolicy = normalizeAgentSessionPolicy(options.agentSessionPolicy);
|
|
1168
1436
|
this.toolOutputLimits = resolveJsonLimits(options.toolOutputLimits);
|
|
1169
1437
|
this.callbackLimits = resolveCallbackLimits(options.callbackLimits);
|
|
1170
1438
|
this.promptQueueLimits = resolvePromptQueueLimits(options.promptQueueLimits);
|
|
1171
1439
|
this.reversionLimits = resolveReversionLimits(options.reversionLimits);
|
|
1172
1440
|
this.turnReversionProvider = options.turnReversionProvider;
|
|
1441
|
+
this.reversionPolicy = options.reversionPolicy ?? 'transcript-optional';
|
|
1442
|
+
if (!['transcript-optional', 'workspace-required'].includes(this.reversionPolicy)) {
|
|
1443
|
+
throw new FlexHarnessValidationError(
|
|
1444
|
+
'reversionPolicy must be "transcript-optional" or "workspace-required".',
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
const protocolVersion = this.turnReversionProvider
|
|
1448
|
+
&& 'protocolVersion' in this.turnReversionProvider
|
|
1449
|
+
? this.turnReversionProvider.protocolVersion
|
|
1450
|
+
: 1;
|
|
1451
|
+
if (protocolVersion !== 1 && protocolVersion !== 2) {
|
|
1452
|
+
throw new FlexHarnessValidationError('Turn reversion provider protocolVersion is invalid.');
|
|
1453
|
+
}
|
|
1454
|
+
if (this.reversionPolicy === 'workspace-required' && protocolVersion !== 2) {
|
|
1455
|
+
throw new FlexHarnessValidationError(
|
|
1456
|
+
'workspace-required reversion policy requires a protocolVersion 2 turn reversion provider.',
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1173
1459
|
this.externalErrorProjector = options.externalErrorProjector;
|
|
1174
1460
|
this.subagents = normalizeSubagents(options.subagents);
|
|
1461
|
+
this.builtInTools = normalizeBuiltInTools(options.builtInTools);
|
|
1175
1462
|
this.slashCommands = normalizeSlashCommands(options.slashCommands);
|
|
1176
1463
|
this.maxSubagentDepth = resolveBoundedPositiveInteger(
|
|
1177
1464
|
options.maxSubagentDepth,
|
|
@@ -1211,7 +1498,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1211
1498
|
const sessionId = options.sessionId ?? plugins.crypto.randomUUID();
|
|
1212
1499
|
validateIdentifier(sessionId, 'sessionId');
|
|
1213
1500
|
requireTransferIdentifier(sessionId, 'sessionId');
|
|
1214
|
-
if (options.title !== undefined)
|
|
1501
|
+
if (options.title !== undefined) {
|
|
1502
|
+
validateUtf8String(
|
|
1503
|
+
options.title,
|
|
1504
|
+
'title',
|
|
1505
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTitleBytes,
|
|
1506
|
+
true,
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1215
1509
|
await resolved.state.scopeQueue;
|
|
1216
1510
|
if (
|
|
1217
1511
|
resolved.state.sessions.has(sessionId)
|
|
@@ -1246,6 +1540,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1246
1540
|
metadata = {
|
|
1247
1541
|
scopeId,
|
|
1248
1542
|
sessionId,
|
|
1543
|
+
sessionGenerationId: createSessionGenerationId(),
|
|
1544
|
+
sessionGenerationSequence: resolved.state.revision + 1,
|
|
1249
1545
|
...(options.title ? { title: options.title } : {}),
|
|
1250
1546
|
createdAt: timestamp,
|
|
1251
1547
|
updatedAt: timestamp,
|
|
@@ -1291,6 +1587,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1291
1587
|
if (!scopeReserved) throw projectedError;
|
|
1292
1588
|
const tombstone: IFlexSessionTombstone = {
|
|
1293
1589
|
sessionId,
|
|
1590
|
+
...requireSessionGeneration(metadata),
|
|
1294
1591
|
deletedAt: new Date().toISOString(),
|
|
1295
1592
|
rootSessionId: sessionId,
|
|
1296
1593
|
depth: 0,
|
|
@@ -1357,6 +1654,41 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1357
1654
|
return publicSnapshot(this.requireSession(state, sessionId).session);
|
|
1358
1655
|
}
|
|
1359
1656
|
|
|
1657
|
+
public async getSessionReversionInfo(
|
|
1658
|
+
scopeId: string,
|
|
1659
|
+
sessionId: string,
|
|
1660
|
+
): Promise<IFlexSessionReversionInfo> {
|
|
1661
|
+
const { state } = await this.resolveState(scopeId);
|
|
1662
|
+
const stored = this.requireSession(state, sessionId);
|
|
1663
|
+
await stored.projectionQueue;
|
|
1664
|
+
this.assertStateAcceptingWork(state);
|
|
1665
|
+
await this.reconcileContextAvailability(state, stored);
|
|
1666
|
+
const unavailableReason = await this.reversionUnavailableReason(state, stored);
|
|
1667
|
+
const groups = this.reversionGroups(stored);
|
|
1668
|
+
const undo = this.reversionUnit(stored, 'undo');
|
|
1669
|
+
const redo = this.reversionUnit(stored, 'redo');
|
|
1670
|
+
const isAvailable = (unit: ReturnType<typeof this.reversionUnit>) => Boolean(
|
|
1671
|
+
!unavailableReason
|
|
1672
|
+
&& unit
|
|
1673
|
+
&& unit.target.contextAvailable
|
|
1674
|
+
&& !unit.segments.some((segment) =>
|
|
1675
|
+
segment.provenance === 'workspace'
|
|
1676
|
+
&& segment.disposition === 'revertible'
|
|
1677
|
+
&& segment.workspaceReference === undefined),
|
|
1678
|
+
);
|
|
1679
|
+
return publicSnapshot({
|
|
1680
|
+
undoAvailable: isAvailable(undo),
|
|
1681
|
+
redoAvailable: isAvailable(redo),
|
|
1682
|
+
groups: groups.map((group, index): IFlexSessionReversionGroup => ({
|
|
1683
|
+
runId: group.target.runId,
|
|
1684
|
+
kind: group.kind,
|
|
1685
|
+
visibility: index < stored.revertCursor ? 'visible' : 'hidden',
|
|
1686
|
+
affectedWorkspaces: cloneSerializable(group.affectedWorkspaces),
|
|
1687
|
+
affectedWorkspacesTruncated: group.affectedWorkspacesTruncated,
|
|
1688
|
+
})),
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1360
1692
|
public async updateSession(
|
|
1361
1693
|
scopeId: string,
|
|
1362
1694
|
sessionId: string,
|
|
@@ -1383,15 +1715,185 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1383
1715
|
return result;
|
|
1384
1716
|
}
|
|
1385
1717
|
|
|
1718
|
+
public async getProjectState(
|
|
1719
|
+
scopeId: string,
|
|
1720
|
+
sessionId: string,
|
|
1721
|
+
): Promise<IFlexProjectStateResult> {
|
|
1722
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1723
|
+
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1724
|
+
return this.projectStateResult(snapshot);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
public async listProjectTasks(
|
|
1728
|
+
scopeId: string,
|
|
1729
|
+
sessionId: string,
|
|
1730
|
+
): Promise<IFlexProjectTasksResult> {
|
|
1731
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1732
|
+
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1733
|
+
return this.projectTasksResult(snapshot, snapshot.tasks);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
public async createProjectTask(
|
|
1737
|
+
scopeId: string,
|
|
1738
|
+
sessionId: string,
|
|
1739
|
+
input: IFlexCreateProjectTaskInput,
|
|
1740
|
+
): Promise<IFlexProjectTaskResult> {
|
|
1741
|
+
validateCreateProjectTaskInput(input);
|
|
1742
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1743
|
+
return this.createProjectTaskInState(
|
|
1744
|
+
state,
|
|
1745
|
+
stored,
|
|
1746
|
+
input,
|
|
1747
|
+
Object.freeze({ actor: 'application' }),
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
public async updateProjectTask(
|
|
1752
|
+
scopeId: string,
|
|
1753
|
+
sessionId: string,
|
|
1754
|
+
input: IFlexUpdateProjectTaskInput,
|
|
1755
|
+
): Promise<IFlexProjectTaskResult> {
|
|
1756
|
+
validateUpdateProjectTaskInput(input);
|
|
1757
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1758
|
+
return this.updateProjectTaskInState(
|
|
1759
|
+
state,
|
|
1760
|
+
stored,
|
|
1761
|
+
input,
|
|
1762
|
+
Object.freeze({ actor: 'application' }),
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
public async deleteProjectTask(
|
|
1767
|
+
scopeId: string,
|
|
1768
|
+
sessionId: string,
|
|
1769
|
+
taskId: string,
|
|
1770
|
+
): Promise<IFlexProjectTaskResult> {
|
|
1771
|
+
validateProjectTaskId(taskId);
|
|
1772
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1773
|
+
return this.deleteProjectTaskInState(
|
|
1774
|
+
state,
|
|
1775
|
+
stored,
|
|
1776
|
+
taskId,
|
|
1777
|
+
Object.freeze({ actor: 'application' }),
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
public async clearProjectTasks(
|
|
1782
|
+
scopeId: string,
|
|
1783
|
+
sessionId: string,
|
|
1784
|
+
): Promise<IFlexProjectTasksResult> {
|
|
1785
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1786
|
+
return this.clearProjectTasksInState(
|
|
1787
|
+
state,
|
|
1788
|
+
stored,
|
|
1789
|
+
Object.freeze({ actor: 'application' }),
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
public async getProjectGoal(
|
|
1794
|
+
scopeId: string,
|
|
1795
|
+
sessionId: string,
|
|
1796
|
+
): Promise<IFlexProjectGoalResult> {
|
|
1797
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1798
|
+
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1799
|
+
return this.projectGoalResult(snapshot);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
public async setProjectGoal(
|
|
1803
|
+
scopeId: string,
|
|
1804
|
+
sessionId: string,
|
|
1805
|
+
goal: string,
|
|
1806
|
+
): Promise<IFlexProjectGoalResult> {
|
|
1807
|
+
validateUtf8String(
|
|
1808
|
+
goal,
|
|
1809
|
+
'project goal',
|
|
1810
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxGoalBytes,
|
|
1811
|
+
true,
|
|
1812
|
+
);
|
|
1813
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1814
|
+
return this.setProjectGoalInState(
|
|
1815
|
+
state,
|
|
1816
|
+
stored,
|
|
1817
|
+
goal,
|
|
1818
|
+
Object.freeze({ actor: 'application' }),
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
public async clearProjectGoal(
|
|
1823
|
+
scopeId: string,
|
|
1824
|
+
sessionId: string,
|
|
1825
|
+
): Promise<IFlexProjectGoalResult> {
|
|
1826
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1827
|
+
return this.clearProjectGoalInState(
|
|
1828
|
+
state,
|
|
1829
|
+
stored,
|
|
1830
|
+
Object.freeze({ actor: 'application' }),
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
public async getProjectScratchpad(
|
|
1835
|
+
scopeId: string,
|
|
1836
|
+
sessionId: string,
|
|
1837
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
1838
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1839
|
+
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1840
|
+
return this.projectScratchpadResult(snapshot);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
public async setProjectScratchpad(
|
|
1844
|
+
scopeId: string,
|
|
1845
|
+
sessionId: string,
|
|
1846
|
+
content: string,
|
|
1847
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
1848
|
+
validateUtf8String(
|
|
1849
|
+
content,
|
|
1850
|
+
'project scratchpad',
|
|
1851
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
1852
|
+
);
|
|
1853
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1854
|
+
return this.setProjectScratchpadInState(
|
|
1855
|
+
state,
|
|
1856
|
+
stored,
|
|
1857
|
+
content,
|
|
1858
|
+
Object.freeze({ actor: 'application' }),
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
public async appendProjectScratchpad(
|
|
1863
|
+
scopeId: string,
|
|
1864
|
+
sessionId: string,
|
|
1865
|
+
content: string,
|
|
1866
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
1867
|
+
validateUtf8String(
|
|
1868
|
+
content,
|
|
1869
|
+
'project scratchpad append',
|
|
1870
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
1871
|
+
);
|
|
1872
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1873
|
+
return this.appendProjectScratchpadInState(
|
|
1874
|
+
state,
|
|
1875
|
+
stored,
|
|
1876
|
+
content,
|
|
1877
|
+
Object.freeze({ actor: 'application' }),
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
public async clearProjectScratchpad(
|
|
1882
|
+
scopeId: string,
|
|
1883
|
+
sessionId: string,
|
|
1884
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
1885
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1886
|
+
return this.clearProjectScratchpadInState(
|
|
1887
|
+
state,
|
|
1888
|
+
stored,
|
|
1889
|
+
Object.freeze({ actor: 'application' }),
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1386
1893
|
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
|
-
}
|
|
1894
|
+
this.assertNoAmbientSlashCommandTeardown({ scopeId, sessionId });
|
|
1391
1895
|
const { scope, state } = await this.resolveState(scopeId);
|
|
1392
|
-
|
|
1393
|
-
throw this.trustInternalError(new FlexHarnessSlashCommandReentryError(invocationOwner.sessionId));
|
|
1394
|
-
}
|
|
1896
|
+
this.assertNoAmbientSlashCommandTeardown({ storageKey: state.storageKey });
|
|
1395
1897
|
validateIdentifier(sessionId, 'sessionId');
|
|
1396
1898
|
await state.scopeQueue;
|
|
1397
1899
|
const deletionKey = state.tombstones.get(sessionId)?.rootSessionId ?? sessionId;
|
|
@@ -1456,6 +1958,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1456
1958
|
});
|
|
1457
1959
|
state.tombstones.set(entrySessionId, {
|
|
1458
1960
|
sessionId: entrySessionId,
|
|
1961
|
+
...requireSessionGeneration(entry.session),
|
|
1459
1962
|
deletedAt,
|
|
1460
1963
|
rootSessionId: sessionId,
|
|
1461
1964
|
depth: (entry.session.depth ?? rootDepth) - rootDepth,
|
|
@@ -1606,7 +2109,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1606
2109
|
sessionId: string,
|
|
1607
2110
|
): Promise<IFlexSlashCommandDescriptor[]> {
|
|
1608
2111
|
const { state } = await this.resolveState(scopeId);
|
|
1609
|
-
return this.trackSlashCommandListing(state, sessionId, async (signal) => {
|
|
2112
|
+
return this.trackSlashCommandListing(scopeId, state, sessionId, async (signal) => {
|
|
1610
2113
|
await this.awaitReversionOperation(state.scopeQueue, signal);
|
|
1611
2114
|
signal.throwIfAborted();
|
|
1612
2115
|
this.assertStateAcceptingWork(state);
|
|
@@ -1644,7 +2147,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1644
2147
|
const workspaceReversion = (unit: ReturnType<typeof this.reversionUnit>) =>
|
|
1645
2148
|
!unit
|
|
1646
2149
|
|| !this.turnReversionProvider
|
|
1647
|
-
|| unit.segments.some((segment) =>
|
|
2150
|
+
|| unit.segments.some((segment) => segment.provenance !== 'workspace')
|
|
1648
2151
|
? 'unsupported' as const
|
|
1649
2152
|
: 'supported' as const;
|
|
1650
2153
|
const reversionAvailability = (unit: ReturnType<typeof this.reversionUnit>) => {
|
|
@@ -1658,7 +2161,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1658
2161
|
unavailableReason: 'The turn is beyond the context archive horizon.',
|
|
1659
2162
|
};
|
|
1660
2163
|
if (unit.segments.some((segment) =>
|
|
1661
|
-
segment.
|
|
2164
|
+
segment.disposition === 'revertible' && segment.workspaceReference === undefined)) return {
|
|
1662
2165
|
available: false,
|
|
1663
2166
|
unavailableReason: 'The turn has no available workspace reversion capture.',
|
|
1664
2167
|
};
|
|
@@ -1802,6 +2305,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1802
2305
|
}
|
|
1803
2306
|
this.requireSessionAvailableForSlashCommand(state, stored, parsed.name);
|
|
1804
2307
|
return this.trackSlashCommandExecution(
|
|
2308
|
+
scopeId,
|
|
1805
2309
|
state,
|
|
1806
2310
|
sessionId,
|
|
1807
2311
|
'operation',
|
|
@@ -1829,6 +2333,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1829
2333
|
this.promptAdmissionByteSize(expanded, promptOptions),
|
|
1830
2334
|
);
|
|
1831
2335
|
return this.trackSlashCommandExecution(
|
|
2336
|
+
scopeId,
|
|
1832
2337
|
state,
|
|
1833
2338
|
sessionId,
|
|
1834
2339
|
'prompt-admission',
|
|
@@ -1925,7 +2430,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1925
2430
|
)) return 'Session already has an active slash command execution.';
|
|
1926
2431
|
if (stored.pendingReversion) return 'Session has a pending reversion operation.';
|
|
1927
2432
|
if (stored.session.agent !== undefined) {
|
|
1928
|
-
return 'Subagent sessions can only be prompted through the foreground
|
|
2433
|
+
return 'Subagent sessions can only be prompted through the foreground delegate tool.';
|
|
1929
2434
|
}
|
|
1930
2435
|
if (stored.outstandingPromptsById.size >= this.promptQueueLimits.maxOutstandingPromptsPerSession) {
|
|
1931
2436
|
return 'Session has reached its outstanding prompt limit.';
|
|
@@ -2001,6 +2506,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2001
2506
|
}
|
|
2002
2507
|
|
|
2003
2508
|
private trackSlashCommandExecution<TResult>(
|
|
2509
|
+
scopeId: string,
|
|
2004
2510
|
state: IStorageState,
|
|
2005
2511
|
sessionId: string,
|
|
2006
2512
|
kind: IActiveSlashCommandExecution['kind'],
|
|
@@ -2024,10 +2530,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2024
2530
|
};
|
|
2025
2531
|
externalSignal?.addEventListener('abort', abortFromExternalSignal, { once: true });
|
|
2026
2532
|
if (externalSignal?.aborted) abortFromExternalSignal();
|
|
2027
|
-
const
|
|
2533
|
+
const activityOwner = Object.freeze({ scopeId, storageKey: state.storageKey, sessionId });
|
|
2534
|
+
const completion = Promise.resolve().then(() => this.slashCommandActivityContext.run(activityOwner, () => {
|
|
2028
2535
|
if (controller.signal.aborted) throw controller.signal.reason;
|
|
2029
2536
|
return operation(controller.signal);
|
|
2030
|
-
});
|
|
2537
|
+
}));
|
|
2031
2538
|
const active: IActiveSlashCommandExecution = {
|
|
2032
2539
|
kind,
|
|
2033
2540
|
storageKey: state.storageKey,
|
|
@@ -2045,12 +2552,15 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2045
2552
|
}
|
|
2046
2553
|
|
|
2047
2554
|
private trackSlashCommandListing<TResult>(
|
|
2555
|
+
scopeId: string,
|
|
2048
2556
|
state: IStorageState,
|
|
2049
2557
|
sessionId: string,
|
|
2050
2558
|
operation: (signal: AbortSignal) => Promise<TResult>,
|
|
2051
2559
|
): Promise<TResult> {
|
|
2052
2560
|
const controller = new AbortController();
|
|
2053
|
-
const
|
|
2561
|
+
const activityOwner = Object.freeze({ scopeId, storageKey: state.storageKey, sessionId });
|
|
2562
|
+
const completion = Promise.resolve().then(() =>
|
|
2563
|
+
this.slashCommandActivityContext.run(activityOwner, () => operation(controller.signal)));
|
|
2054
2564
|
const active: IActiveSlashCommandListing = {
|
|
2055
2565
|
storageKey: state.storageKey,
|
|
2056
2566
|
sessionId,
|
|
@@ -2094,6 +2604,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2094
2604
|
sessionKey,
|
|
2095
2605
|
});
|
|
2096
2606
|
return this.trackSlashCommandExecution(
|
|
2607
|
+
scopeId,
|
|
2097
2608
|
state,
|
|
2098
2609
|
stored.session.sessionId,
|
|
2099
2610
|
'handler',
|
|
@@ -2403,6 +2914,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2403
2914
|
}
|
|
2404
2915
|
this.requireSessionAvailableForSlashCommand(state, stored, 'compact');
|
|
2405
2916
|
await this.trackSlashCommandExecution(
|
|
2917
|
+
scopeId,
|
|
2406
2918
|
state,
|
|
2407
2919
|
sessionId,
|
|
2408
2920
|
'operation',
|
|
@@ -2464,24 +2976,77 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2464
2976
|
stored: IStoredSessionState,
|
|
2465
2977
|
direction: 'undo' | 'redo',
|
|
2466
2978
|
): { target: IFlexReversionSegment; segments: IFlexReversionSegment[]; toCursor: number } | undefined {
|
|
2467
|
-
const
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2979
|
+
const groups = this.reversionGroups(stored);
|
|
2980
|
+
if (direction === 'undo') {
|
|
2981
|
+
let start = stored.revertCursor - 1;
|
|
2982
|
+
while (start >= 0 && groups[start]?.kind === 'no-change') start--;
|
|
2983
|
+
const candidate = groups[start];
|
|
2984
|
+
if (!candidate || candidate.kind !== 'candidate') return undefined;
|
|
2985
|
+
return {
|
|
2986
|
+
target: candidate.target,
|
|
2987
|
+
segments: groups.slice(start, stored.revertCursor).flatMap((group) => group.segments),
|
|
2988
|
+
toCursor: start,
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
const candidate = groups[stored.revertCursor];
|
|
2992
|
+
if (!candidate || candidate.kind !== 'candidate') return undefined;
|
|
2993
|
+
let end = stored.revertCursor + 1;
|
|
2994
|
+
while (groups[end]?.kind === 'no-change') end++;
|
|
2478
2995
|
return {
|
|
2479
|
-
target: candidate,
|
|
2480
|
-
segments:
|
|
2481
|
-
toCursor:
|
|
2996
|
+
target: candidate.target,
|
|
2997
|
+
segments: groups.slice(stored.revertCursor, end).flatMap((group) => group.segments),
|
|
2998
|
+
toCursor: end,
|
|
2482
2999
|
};
|
|
2483
3000
|
}
|
|
2484
3001
|
|
|
3002
|
+
private reversionGroups(stored: IStoredSessionState): IReversionGroup[] {
|
|
3003
|
+
const groups: IReversionGroup[] = [];
|
|
3004
|
+
let leading: IFlexReversionSegment[] = [];
|
|
3005
|
+
for (const segment of stored.reversionSegments) {
|
|
3006
|
+
if (segment.status === 'completed') {
|
|
3007
|
+
const segments = groups.length === 0 ? [...leading, segment] : [segment];
|
|
3008
|
+
leading = [];
|
|
3009
|
+
groups.push({
|
|
3010
|
+
target: segment,
|
|
3011
|
+
segments,
|
|
3012
|
+
kind: 'candidate',
|
|
3013
|
+
affectedWorkspaces: [],
|
|
3014
|
+
affectedWorkspacesTruncated: false,
|
|
3015
|
+
});
|
|
3016
|
+
} else if (groups.length > 0) {
|
|
3017
|
+
groups[groups.length - 1].segments.push(segment);
|
|
3018
|
+
} else {
|
|
3019
|
+
leading.push(segment);
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
for (const group of groups) {
|
|
3023
|
+
const affected = new Map<string, IFlexAffectedWorkspace>();
|
|
3024
|
+
for (const workspace of group.segments.flatMap((segment) =>
|
|
3025
|
+
segment.affectedWorkspaces ?? [])) {
|
|
3026
|
+
if (affected.has(workspace.id)) continue;
|
|
3027
|
+
if (affected.size < FLEX_REVERSION_MAX_AFFECTED_WORKSPACES) {
|
|
3028
|
+
affected.set(workspace.id, cloneSerializable(workspace));
|
|
3029
|
+
} else {
|
|
3030
|
+
group.affectedWorkspacesTruncated = true;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
group.affectedWorkspaces = [...affected.values()];
|
|
3034
|
+
if (this.reversionPolicy === 'transcript-optional') continue;
|
|
3035
|
+
if (group.segments.some((segment) =>
|
|
3036
|
+
segment.status === 'capturing'
|
|
3037
|
+
|| segment.disposition === 'pending'
|
|
3038
|
+
|| (segment.protocolVersion === 1 && segment.provenance === 'transcript')
|
|
3039
|
+
|| segment.disposition === 'nonrevertible')) {
|
|
3040
|
+
group.kind = 'barrier';
|
|
3041
|
+
} else if (group.segments.some((segment) => segment.disposition === 'revertible')) {
|
|
3042
|
+
group.kind = 'candidate';
|
|
3043
|
+
} else {
|
|
3044
|
+
group.kind = 'no-change';
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
return groups;
|
|
3048
|
+
}
|
|
3049
|
+
|
|
2485
3050
|
private async requireReversionIdle(
|
|
2486
3051
|
state: IStorageState,
|
|
2487
3052
|
stored: IStoredSessionState,
|
|
@@ -2587,6 +3152,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2587
3152
|
throw this.trustInternalError(new FlexHarnessSlashCommandReentryError(sessionId));
|
|
2588
3153
|
}
|
|
2589
3154
|
return this.trackSlashCommandExecution(
|
|
3155
|
+
scopeId,
|
|
2590
3156
|
state,
|
|
2591
3157
|
sessionId,
|
|
2592
3158
|
'operation',
|
|
@@ -2609,8 +3175,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2609
3175
|
if (pending.direction !== direction) {
|
|
2610
3176
|
throw new FlexHarnessSessionBusyError(sessionId, 'has a pending opposite reversion operation');
|
|
2611
3177
|
}
|
|
2612
|
-
const
|
|
2613
|
-
|
|
3178
|
+
const target = pending.segmentRunIds
|
|
3179
|
+
.map((runId) => stored.reversionSegments.find((segment) => segment.runId === runId))
|
|
3180
|
+
.find((segment) => segment?.status === 'completed');
|
|
2614
3181
|
if (!target) throw new FlexHarnessValidationError('Pending reversion has no target candidate.');
|
|
2615
3182
|
await this.resumePendingApply(state, stored, scopeId, scope, operationSignal);
|
|
2616
3183
|
this.emitEvent(scopeId, sessionId, {
|
|
@@ -2634,7 +3201,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2634
3201
|
);
|
|
2635
3202
|
}
|
|
2636
3203
|
if (this.turnReversionProvider && unit.segments.some((segment) =>
|
|
2637
|
-
segment.
|
|
3204
|
+
segment.disposition === 'revertible' && segment.workspaceReference === undefined)) {
|
|
2638
3205
|
throw new FlexHarnessSlashCommandUnavailableError(
|
|
2639
3206
|
direction,
|
|
2640
3207
|
'The turn has no available workspace reversion capture.',
|
|
@@ -2713,7 +3280,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2713
3280
|
if (pending?.kind !== 'apply') return;
|
|
2714
3281
|
const segmentMap = new Map(stored.reversionSegments.map((segment) => [segment.runId, segment]));
|
|
2715
3282
|
if (
|
|
2716
|
-
pending.segmentRunIds.some((runId) =>
|
|
3283
|
+
pending.segmentRunIds.some((runId) =>
|
|
3284
|
+
segmentMap.get(runId)?.disposition === 'revertible')
|
|
2717
3285
|
&& !this.turnReversionProvider
|
|
2718
3286
|
) {
|
|
2719
3287
|
throw new FlexHarnessValidationError(
|
|
@@ -2723,18 +3291,25 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2723
3291
|
const orderedRunIds = pending.direction === 'undo'
|
|
2724
3292
|
? [...pending.segmentRunIds].reverse()
|
|
2725
3293
|
: [...pending.segmentRunIds];
|
|
3294
|
+
let workspaceProgress = pending.appliedRunIds.some((runId) =>
|
|
3295
|
+
segmentMap.get(runId)?.disposition === 'revertible');
|
|
2726
3296
|
for (const runId of orderedRunIds) {
|
|
2727
3297
|
if (pending.appliedRunIds.includes(runId)) continue;
|
|
2728
|
-
if (signal?.aborted) {
|
|
3298
|
+
if (signal?.aborted && !workspaceProgress) {
|
|
2729
3299
|
await this.persistKnownNotApplied(state, stored, pending.operationId, runId);
|
|
2730
3300
|
throw signal.reason;
|
|
2731
3301
|
}
|
|
2732
3302
|
const segment = segmentMap.get(runId);
|
|
2733
3303
|
if (!segment) throw new FlexHarnessValidationError('Pending reversion references a missing segment.');
|
|
2734
|
-
if (segment.
|
|
3304
|
+
if (segment.disposition === 'revertible') {
|
|
2735
3305
|
if (!this.turnReversionProvider || !segment.captureId || segment.workspaceReference === undefined) {
|
|
2736
3306
|
throw new FlexHarnessValidationError('Pending workspace reversion segment is incomplete.');
|
|
2737
3307
|
}
|
|
3308
|
+
if (this.reversionProviderProtocolVersion() !== segment.protocolVersion) {
|
|
3309
|
+
throw new FlexHarnessValidationError(
|
|
3310
|
+
`Pending workspace reversion requires protocolVersion ${segment.protocolVersion}.`,
|
|
3311
|
+
);
|
|
3312
|
+
}
|
|
2738
3313
|
const captureId = segment.captureId;
|
|
2739
3314
|
const reference = segment.workspaceReference;
|
|
2740
3315
|
const childOperationId = `${pending.operationId}:${sha256Hex(runId).slice(0, 16)}`;
|
|
@@ -2770,7 +3345,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2770
3345
|
}
|
|
2771
3346
|
if (inspection.status === 'not-applied') {
|
|
2772
3347
|
try {
|
|
2773
|
-
if (signal) {
|
|
3348
|
+
if (signal && !workspaceProgress) {
|
|
2774
3349
|
await this.turnReversionProvider.apply(createContext(signal));
|
|
2775
3350
|
} else {
|
|
2776
3351
|
await this.withReversionMaintenanceSignal((maintenanceSignal) =>
|
|
@@ -2811,6 +3386,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2811
3386
|
}
|
|
2812
3387
|
if (!current.appliedRunIds.includes(runId)) current.appliedRunIds.push(runId);
|
|
2813
3388
|
}, true);
|
|
3389
|
+
if (segment.disposition === 'revertible') workspaceProgress = true;
|
|
2814
3390
|
}
|
|
2815
3391
|
await this.mutateProjection(state, stored, () => {
|
|
2816
3392
|
const current = stored.pendingReversion;
|
|
@@ -2834,7 +3410,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2834
3410
|
throw new FlexHarnessValidationError('Pending reversion changed while recording failed apply.');
|
|
2835
3411
|
}
|
|
2836
3412
|
current.appliedRunIds = current.appliedRunIds.filter((entry) => entry !== runId);
|
|
2837
|
-
|
|
3413
|
+
const hasWorkspaceProgress = current.appliedRunIds.some((appliedRunId) =>
|
|
3414
|
+
stored.reversionSegments.some((segment) =>
|
|
3415
|
+
segment.runId === appliedRunId && segment.disposition === 'revertible'));
|
|
3416
|
+
if (!hasWorkspaceProgress) {
|
|
2838
3417
|
stored.revertCursor = current.fromCursor;
|
|
2839
3418
|
delete stored.pendingReversion;
|
|
2840
3419
|
}
|
|
@@ -2852,17 +3431,31 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2852
3431
|
if (pending?.kind !== 'capture' || !this.turnReversionProvider) return;
|
|
2853
3432
|
const segment = stored.reversionSegments.find((entry) => entry.runId === pending.runId);
|
|
2854
3433
|
if (!segment) throw new FlexHarnessValidationError('Pending capture references a missing segment.');
|
|
3434
|
+
if (this.reversionProviderProtocolVersion() !== pending.protocolVersion) {
|
|
3435
|
+
throw new FlexHarnessValidationError(
|
|
3436
|
+
`Pending capture recovery requires protocolVersion ${pending.protocolVersion}.`,
|
|
3437
|
+
);
|
|
3438
|
+
}
|
|
2855
3439
|
const outcome = outcomes.get(pending.runId);
|
|
2856
|
-
|
|
3440
|
+
if (pending.state === 'prepared') {
|
|
3441
|
+
await this.mutateProjection(state, stored, () => {
|
|
3442
|
+
const current = stored.pendingReversion;
|
|
3443
|
+
if (current?.kind !== 'capture' || current.captureId !== pending.captureId) {
|
|
3444
|
+
throw new FlexHarnessValidationError('Pending capture changed before finalization.');
|
|
3445
|
+
}
|
|
3446
|
+
current.state = 'finalizing';
|
|
3447
|
+
}, true);
|
|
3448
|
+
}
|
|
3449
|
+
const finalizedOutcome = await this.resolveReversionCaptureOutcome(
|
|
2857
3450
|
state,
|
|
2858
3451
|
scopeId,
|
|
2859
3452
|
scope,
|
|
2860
3453
|
stored.session.sessionId,
|
|
2861
3454
|
pending.runId,
|
|
2862
3455
|
pending.captureId,
|
|
2863
|
-
pending.state,
|
|
3456
|
+
pending.state === 'prepared' ? 'finalizing' : pending.state,
|
|
2864
3457
|
);
|
|
2865
|
-
if (
|
|
3458
|
+
if (finalizedOutcome === undefined) {
|
|
2866
3459
|
await this.mutateProjection(state, stored, () => {
|
|
2867
3460
|
stored.reversionSegments = stored.reversionSegments.filter(
|
|
2868
3461
|
(entry) => entry.runId !== pending.runId,
|
|
@@ -2877,7 +3470,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2877
3470
|
: outcome === 'rejected'
|
|
2878
3471
|
? 'failed'
|
|
2879
3472
|
: 'cancelled';
|
|
2880
|
-
|
|
3473
|
+
this.applyFinalizedReversionOutcome(stored, segment, finalizedOutcome);
|
|
2881
3474
|
segment.eventIds = stored.agentSession.getEvents()
|
|
2882
3475
|
.filter((event) => event.generationId === pending.runId)
|
|
2883
3476
|
.map((event) => event.id);
|
|
@@ -3017,10 +3610,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3017
3610
|
}
|
|
3018
3611
|
|
|
3019
3612
|
public retireScope(scopeId: string): Promise<void> {
|
|
3020
|
-
|
|
3021
|
-
if (invocationOwner?.scopeId === scopeId) {
|
|
3022
|
-
throw this.trustInternalError(new FlexHarnessSlashCommandReentryError(invocationOwner.sessionId));
|
|
3023
|
-
}
|
|
3613
|
+
this.assertNoAmbientSlashCommandTeardown({ scopeId });
|
|
3024
3614
|
this.assertOpen();
|
|
3025
3615
|
validateIdentifier(scopeId, 'scopeId');
|
|
3026
3616
|
const existing = this.scopeRetirements.get(scopeId);
|
|
@@ -3039,13 +3629,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3039
3629
|
}
|
|
3040
3630
|
|
|
3041
3631
|
public async dispose(): Promise<void> {
|
|
3632
|
+
this.assertNoAmbientSlashCommandTeardown();
|
|
3042
3633
|
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
3634
|
this.closed = true;
|
|
3050
3635
|
let disposal!: Promise<void>;
|
|
3051
3636
|
disposal = this.disposeInternal().catch((error) => {
|
|
@@ -3125,7 +3710,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3125
3710
|
}
|
|
3126
3711
|
if (stored.session.agent !== undefined && !subagentAdmission) {
|
|
3127
3712
|
throw new FlexHarnessValidationError(
|
|
3128
|
-
'Subagent sessions can only be prompted through the foreground
|
|
3713
|
+
'Subagent sessions can only be prompted through the foreground delegate tool.',
|
|
3129
3714
|
);
|
|
3130
3715
|
}
|
|
3131
3716
|
if (stored.outstandingPromptsById.size >= this.promptQueueLimits.maxOutstandingPromptsPerSession) {
|
|
@@ -3339,6 +3924,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3339
3924
|
const captureId = this.turnReversionProvider
|
|
3340
3925
|
? this.reversionId('capture', run.state.storageKey, run.sessionId, run.runId)
|
|
3341
3926
|
: undefined;
|
|
3927
|
+
const protocolVersion = this.reversionProviderProtocolVersion() ?? 1;
|
|
3342
3928
|
run.stored.reversionSegments.push({
|
|
3343
3929
|
runId: run.runId,
|
|
3344
3930
|
userMessageId: reservation.userMessage.messageId,
|
|
@@ -3346,6 +3932,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3346
3932
|
contextAvailable: true,
|
|
3347
3933
|
eventIds: [],
|
|
3348
3934
|
workspaceCaptured: captureId !== undefined,
|
|
3935
|
+
protocolVersion,
|
|
3936
|
+
provenance: captureId === undefined ? 'transcript' : 'workspace',
|
|
3937
|
+
...(captureId === undefined ? {} : { disposition: 'pending' }),
|
|
3349
3938
|
...(captureId === undefined ? {} : { captureId }),
|
|
3350
3939
|
});
|
|
3351
3940
|
if (captureId) {
|
|
@@ -3354,6 +3943,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3354
3943
|
runId: run.runId,
|
|
3355
3944
|
captureId,
|
|
3356
3945
|
state: 'preparing',
|
|
3946
|
+
protocolVersion,
|
|
3357
3947
|
};
|
|
3358
3948
|
}
|
|
3359
3949
|
}
|
|
@@ -3727,32 +4317,438 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3727
4317
|
throw errors.length > 1 ? combineErrors(errors) : terminalError;
|
|
3728
4318
|
}
|
|
3729
4319
|
|
|
3730
|
-
private
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
): Promise<plugins.IAgentGenerationLease> {
|
|
3735
|
-
signal.throwIfAborted();
|
|
3736
|
-
await this.prepareRunReversion(run, signal);
|
|
3737
|
-
run.phase = 'running';
|
|
3738
|
-
const queued = run.stored.outstandingPromptsById.get(run.queueId);
|
|
3739
|
-
if (queued && (queued.status === 'starting' || queued.status === 'scheduled')) {
|
|
3740
|
-
queued.status = 'running';
|
|
3741
|
-
this.emitPromptQueueEvent(queued, 'prompt.running');
|
|
4320
|
+
private requireProjectManagementTools(): INormalizedProjectManagementTools {
|
|
4321
|
+
const projectManagement = this.builtInTools.projectManagement;
|
|
4322
|
+
if (!projectManagement) {
|
|
4323
|
+
throw new FlexHarnessValidationError('Project management is not configured for this FlexHarness.');
|
|
3742
4324
|
}
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
4325
|
+
return projectManagement;
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4328
|
+
private projectManagementKey(storageKey: string, sessionId: string): string {
|
|
4329
|
+
return JSON.stringify([storageKey, sessionId]);
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
private queueProjectManagementOperation<TResult>(
|
|
4333
|
+
storageKey: string,
|
|
4334
|
+
sessionId: string,
|
|
4335
|
+
operation: () => Promise<TResult>,
|
|
4336
|
+
): Promise<TResult> {
|
|
4337
|
+
const key = this.projectManagementKey(storageKey, sessionId);
|
|
4338
|
+
const previous = this.projectManagementQueues.get(key) ?? Promise.resolve();
|
|
4339
|
+
const result = previous.then(operation);
|
|
4340
|
+
const barrier = result.then(() => undefined, () => undefined);
|
|
4341
|
+
this.projectManagementQueues.set(key, barrier);
|
|
4342
|
+
void barrier.then(() => {
|
|
4343
|
+
if (this.projectManagementQueues.get(key) === barrier) {
|
|
4344
|
+
this.projectManagementQueues.delete(key);
|
|
4345
|
+
}
|
|
4346
|
+
});
|
|
4347
|
+
return result;
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4350
|
+
private async awaitProjectManagementOperations(
|
|
4351
|
+
storageKey: string,
|
|
4352
|
+
sessionIds: Iterable<string>,
|
|
4353
|
+
): Promise<void> {
|
|
4354
|
+
const barriers = [...new Set(sessionIds)]
|
|
4355
|
+
.map((sessionId) => this.projectManagementQueues.get(
|
|
4356
|
+
this.projectManagementKey(storageKey, sessionId),
|
|
4357
|
+
))
|
|
4358
|
+
.filter((barrier): barrier is Promise<void> => barrier !== undefined);
|
|
4359
|
+
await Promise.all(barriers);
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
private async resolveProjectManagementSession(
|
|
4363
|
+
scopeId: string,
|
|
4364
|
+
sessionId: string,
|
|
4365
|
+
): Promise<{ state: IStorageState; stored: IStoredSessionState }> {
|
|
4366
|
+
this.requireProjectManagementTools();
|
|
4367
|
+
const { state } = await this.resolveState(scopeId);
|
|
4368
|
+
await state.scopeQueue;
|
|
4369
|
+
this.assertOpen();
|
|
4370
|
+
this.assertStateAcceptingWork(state);
|
|
4371
|
+
return { state, stored: this.requireSession(state, sessionId) };
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
private assertProjectManagementSession(
|
|
4375
|
+
state: IStorageState,
|
|
4376
|
+
stored: IStoredSessionState,
|
|
4377
|
+
): void {
|
|
4378
|
+
this.assertOpen();
|
|
4379
|
+
this.assertStateAcceptingWork(state);
|
|
4380
|
+
if (
|
|
4381
|
+
state.sessions.get(stored.session.sessionId) !== stored
|
|
4382
|
+
|| state.tombstones.has(stored.session.sessionId)
|
|
4383
|
+
) {
|
|
4384
|
+
throw new FlexHarnessNotFoundError('Session', stored.session.sessionId);
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
|
|
4388
|
+
private async loadProjectManagementState(
|
|
4389
|
+
state: IStorageState,
|
|
4390
|
+
stored: IStoredSessionState,
|
|
4391
|
+
): Promise<IFlexProjectManagementSnapshot> {
|
|
4392
|
+
const store = this.stores.projectManagement;
|
|
4393
|
+
const generation = requireSessionGeneration(stored.session);
|
|
4394
|
+
const record = await store.load(state.storageKey, stored.session.sessionId);
|
|
4395
|
+
if (record === undefined) {
|
|
4396
|
+
return createEmptyFlexProjectManagementSnapshot(
|
|
4397
|
+
generation.sessionGenerationId,
|
|
4398
|
+
generation.sessionGenerationSequence,
|
|
4399
|
+
);
|
|
4400
|
+
}
|
|
4401
|
+
assertFlexProjectManagementRecord(record);
|
|
4402
|
+
if (record.sessionGenerationId === generation.sessionGenerationId) {
|
|
4403
|
+
if (record.sessionGenerationSequence !== generation.sessionGenerationSequence) {
|
|
4404
|
+
throw new FlexHarnessStoreConflictError(
|
|
4405
|
+
this.projectManagementKey(state.storageKey, stored.session.sessionId),
|
|
4406
|
+
0,
|
|
4407
|
+
record.revision,
|
|
4408
|
+
);
|
|
4409
|
+
}
|
|
4410
|
+
if (!('deletedAt' in record)) return cloneSerializable(record);
|
|
4411
|
+
throw new FlexHarnessNotFoundError('Project management state', stored.session.sessionId);
|
|
4412
|
+
}
|
|
4413
|
+
if (
|
|
4414
|
+
'deletedAt' in record
|
|
4415
|
+
&& record.sessionGenerationSequence < generation.sessionGenerationSequence
|
|
4416
|
+
) {
|
|
4417
|
+
return createEmptyFlexProjectManagementSnapshot(
|
|
4418
|
+
generation.sessionGenerationId,
|
|
4419
|
+
generation.sessionGenerationSequence,
|
|
4420
|
+
);
|
|
4421
|
+
}
|
|
4422
|
+
throw new FlexHarnessStoreConflictError(
|
|
4423
|
+
this.projectManagementKey(state.storageKey, stored.session.sessionId),
|
|
4424
|
+
0,
|
|
4425
|
+
record.revision,
|
|
4426
|
+
);
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4429
|
+
private readProjectManagementState(
|
|
4430
|
+
state: IStorageState,
|
|
4431
|
+
stored: IStoredSessionState,
|
|
4432
|
+
): Promise<IFlexProjectManagementSnapshot> {
|
|
4433
|
+
return this.queueProjectManagementOperation(
|
|
4434
|
+
state.storageKey,
|
|
4435
|
+
stored.session.sessionId,
|
|
4436
|
+
async () => {
|
|
4437
|
+
this.assertProjectManagementSession(state, stored);
|
|
4438
|
+
const snapshot = await this.loadProjectManagementState(state, stored);
|
|
4439
|
+
this.assertProjectManagementSession(state, stored);
|
|
4440
|
+
return publicSnapshot(snapshot);
|
|
4441
|
+
},
|
|
4442
|
+
);
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
private mutateProjectManagementState<TResult>(
|
|
4446
|
+
state: IStorageState,
|
|
4447
|
+
stored: IStoredSessionState,
|
|
4448
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4449
|
+
mutation: (
|
|
4450
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
4451
|
+
) => { changed: boolean; value: TResult },
|
|
4452
|
+
): Promise<{ snapshot: IFlexProjectManagementSnapshot; value: TResult }> {
|
|
4453
|
+
return this.queueProjectManagementOperation(
|
|
4454
|
+
state.storageKey,
|
|
4455
|
+
stored.session.sessionId,
|
|
4456
|
+
async () => {
|
|
4457
|
+
this.assertProjectManagementSession(state, stored);
|
|
4458
|
+
const snapshot = await this.loadProjectManagementState(state, stored);
|
|
4459
|
+
this.assertProjectManagementSession(state, stored);
|
|
4460
|
+
const expectedRevision = snapshot.revision;
|
|
4461
|
+
const result = mutation(snapshot);
|
|
4462
|
+
if (result.changed) {
|
|
4463
|
+
snapshot.revision = expectedRevision + 1;
|
|
4464
|
+
assertFlexProjectManagementSnapshot(snapshot);
|
|
4465
|
+
this.assertProjectManagementSession(state, stored);
|
|
4466
|
+
await this.stores.projectManagement.save(
|
|
4467
|
+
state.storageKey,
|
|
4468
|
+
stored.session.sessionId,
|
|
4469
|
+
cloneSerializable(snapshot),
|
|
4470
|
+
expectedRevision,
|
|
4471
|
+
Object.freeze(cloneSerializable(writeContext)),
|
|
4472
|
+
);
|
|
4473
|
+
}
|
|
4474
|
+
return {
|
|
4475
|
+
snapshot: publicSnapshot(snapshot),
|
|
4476
|
+
value: result.value === undefined ? result.value : publicSnapshot(result.value),
|
|
4477
|
+
};
|
|
4478
|
+
},
|
|
4479
|
+
);
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4482
|
+
private projectStateResult(snapshot: IFlexProjectManagementSnapshot): IFlexProjectStateResult {
|
|
4483
|
+
return publicSnapshot({ revision: snapshot.revision, state: snapshot });
|
|
4484
|
+
}
|
|
4485
|
+
|
|
4486
|
+
private projectTaskResult(
|
|
4487
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
4488
|
+
task: IFlexProjectTask,
|
|
4489
|
+
): IFlexProjectTaskResult {
|
|
4490
|
+
return publicSnapshot({ revision: snapshot.revision, task, state: snapshot });
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
private projectTasksResult(
|
|
4494
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
4495
|
+
tasks: IFlexProjectTask[],
|
|
4496
|
+
): IFlexProjectTasksResult {
|
|
4497
|
+
return publicSnapshot({ revision: snapshot.revision, tasks, state: snapshot });
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4500
|
+
private projectGoalResult(snapshot: IFlexProjectManagementSnapshot): IFlexProjectGoalResult {
|
|
4501
|
+
return publicSnapshot({
|
|
4502
|
+
revision: snapshot.revision,
|
|
4503
|
+
goal: snapshot.goal ?? null,
|
|
4504
|
+
state: snapshot,
|
|
4505
|
+
});
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4508
|
+
private projectScratchpadResult(
|
|
4509
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
4510
|
+
): IFlexProjectScratchpadResult {
|
|
4511
|
+
return publicSnapshot({
|
|
4512
|
+
revision: snapshot.revision,
|
|
4513
|
+
scratchpad: snapshot.scratchpad,
|
|
4514
|
+
state: snapshot,
|
|
4515
|
+
});
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
private async createProjectTaskInState(
|
|
4519
|
+
state: IStorageState,
|
|
4520
|
+
stored: IStoredSessionState,
|
|
4521
|
+
input: IFlexCreateProjectTaskInput,
|
|
4522
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4523
|
+
): Promise<IFlexProjectTaskResult> {
|
|
4524
|
+
const status = input.status ?? 'pending';
|
|
4525
|
+
const priority = input.priority ?? 'medium';
|
|
4526
|
+
const result = await this.mutateProjectManagementState(
|
|
4527
|
+
state,
|
|
4528
|
+
stored,
|
|
4529
|
+
writeContext,
|
|
4530
|
+
(snapshot) => {
|
|
4531
|
+
const existing = snapshot.tasks.find((task) => task.id === input.id);
|
|
4532
|
+
if (existing) {
|
|
4533
|
+
if (
|
|
4534
|
+
existing.content === input.content
|
|
4535
|
+
&& existing.status === status
|
|
4536
|
+
&& existing.priority === priority
|
|
4537
|
+
) return { changed: false, value: existing };
|
|
4538
|
+
throw new FlexHarnessValidationError(
|
|
4539
|
+
`Project task "${input.id}" already exists with different creation data.`,
|
|
4540
|
+
);
|
|
4541
|
+
}
|
|
4542
|
+
if (snapshot.tasks.length >= FLEX_PROJECT_MANAGEMENT_LIMITS.maxTasks) {
|
|
4543
|
+
throw new FlexHarnessValidationError('Project task count has reached its limit.');
|
|
4544
|
+
}
|
|
4545
|
+
const timestamp = new Date().toISOString();
|
|
4546
|
+
const task: IFlexProjectTask = {
|
|
4547
|
+
id: input.id,
|
|
4548
|
+
content: input.content,
|
|
4549
|
+
status,
|
|
4550
|
+
priority,
|
|
4551
|
+
createdAt: timestamp,
|
|
4552
|
+
updatedAt: timestamp,
|
|
4553
|
+
};
|
|
4554
|
+
snapshot.tasks.push(task);
|
|
4555
|
+
return { changed: true, value: task };
|
|
4556
|
+
},
|
|
4557
|
+
);
|
|
4558
|
+
return this.projectTaskResult(result.snapshot, result.value);
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
private async updateProjectTaskInState(
|
|
4562
|
+
state: IStorageState,
|
|
4563
|
+
stored: IStoredSessionState,
|
|
4564
|
+
input: IFlexUpdateProjectTaskInput,
|
|
4565
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4566
|
+
): Promise<IFlexProjectTaskResult> {
|
|
4567
|
+
const result = await this.mutateProjectManagementState(
|
|
4568
|
+
state,
|
|
4569
|
+
stored,
|
|
4570
|
+
writeContext,
|
|
4571
|
+
(snapshot) => {
|
|
4572
|
+
const task = snapshot.tasks.find((entry) => entry.id === input.id);
|
|
4573
|
+
if (!task) throw new FlexHarnessNotFoundError('Project task', input.id);
|
|
4574
|
+
const changed = (input.content !== undefined && input.content !== task.content)
|
|
4575
|
+
|| (input.status !== undefined && input.status !== task.status)
|
|
4576
|
+
|| (input.priority !== undefined && input.priority !== task.priority);
|
|
4577
|
+
if (!changed) return { changed: false, value: task };
|
|
4578
|
+
if (input.content !== undefined) task.content = input.content;
|
|
4579
|
+
if (input.status !== undefined) task.status = input.status;
|
|
4580
|
+
if (input.priority !== undefined) task.priority = input.priority;
|
|
4581
|
+
task.updatedAt = new Date().toISOString();
|
|
4582
|
+
return { changed: true, value: task };
|
|
4583
|
+
},
|
|
4584
|
+
);
|
|
4585
|
+
return this.projectTaskResult(result.snapshot, result.value);
|
|
4586
|
+
}
|
|
4587
|
+
|
|
4588
|
+
private async deleteProjectTaskInState(
|
|
4589
|
+
state: IStorageState,
|
|
4590
|
+
stored: IStoredSessionState,
|
|
4591
|
+
taskId: string,
|
|
4592
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4593
|
+
): Promise<IFlexProjectTaskResult> {
|
|
4594
|
+
const result = await this.mutateProjectManagementState(
|
|
4595
|
+
state,
|
|
4596
|
+
stored,
|
|
4597
|
+
writeContext,
|
|
4598
|
+
(snapshot) => {
|
|
4599
|
+
const index = snapshot.tasks.findIndex((task) => task.id === taskId);
|
|
4600
|
+
if (index < 0) throw new FlexHarnessNotFoundError('Project task', taskId);
|
|
4601
|
+
const [task] = snapshot.tasks.splice(index, 1);
|
|
4602
|
+
return { changed: true, value: task };
|
|
4603
|
+
},
|
|
4604
|
+
);
|
|
4605
|
+
return this.projectTaskResult(result.snapshot, result.value);
|
|
4606
|
+
}
|
|
4607
|
+
|
|
4608
|
+
private async clearProjectTasksInState(
|
|
4609
|
+
state: IStorageState,
|
|
4610
|
+
stored: IStoredSessionState,
|
|
4611
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4612
|
+
): Promise<IFlexProjectTasksResult> {
|
|
4613
|
+
const result = await this.mutateProjectManagementState(
|
|
4614
|
+
state,
|
|
4615
|
+
stored,
|
|
4616
|
+
writeContext,
|
|
4617
|
+
(snapshot) => {
|
|
4618
|
+
const tasks = snapshot.tasks;
|
|
4619
|
+
if (tasks.length === 0) return { changed: false, value: [] as IFlexProjectTask[] };
|
|
4620
|
+
snapshot.tasks = [];
|
|
4621
|
+
return { changed: true, value: tasks };
|
|
4622
|
+
},
|
|
4623
|
+
);
|
|
4624
|
+
return this.projectTasksResult(result.snapshot, result.value);
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
private async setProjectGoalInState(
|
|
4628
|
+
state: IStorageState,
|
|
4629
|
+
stored: IStoredSessionState,
|
|
4630
|
+
goal: string,
|
|
4631
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4632
|
+
): Promise<IFlexProjectGoalResult> {
|
|
4633
|
+
const result = await this.mutateProjectManagementState(
|
|
4634
|
+
state,
|
|
4635
|
+
stored,
|
|
4636
|
+
writeContext,
|
|
4637
|
+
(snapshot) => {
|
|
4638
|
+
if (snapshot.goal === goal) return { changed: false, value: undefined };
|
|
4639
|
+
snapshot.goal = goal;
|
|
4640
|
+
return { changed: true, value: undefined };
|
|
4641
|
+
},
|
|
4642
|
+
);
|
|
4643
|
+
return this.projectGoalResult(result.snapshot);
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
private async clearProjectGoalInState(
|
|
4647
|
+
state: IStorageState,
|
|
4648
|
+
stored: IStoredSessionState,
|
|
4649
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4650
|
+
): Promise<IFlexProjectGoalResult> {
|
|
4651
|
+
const result = await this.mutateProjectManagementState(
|
|
4652
|
+
state,
|
|
4653
|
+
stored,
|
|
4654
|
+
writeContext,
|
|
4655
|
+
(snapshot) => {
|
|
4656
|
+
if (snapshot.goal === undefined) return { changed: false, value: undefined };
|
|
4657
|
+
delete snapshot.goal;
|
|
4658
|
+
return { changed: true, value: undefined };
|
|
4659
|
+
},
|
|
4660
|
+
);
|
|
4661
|
+
return this.projectGoalResult(result.snapshot);
|
|
4662
|
+
}
|
|
4663
|
+
|
|
4664
|
+
private async setProjectScratchpadInState(
|
|
4665
|
+
state: IStorageState,
|
|
4666
|
+
stored: IStoredSessionState,
|
|
4667
|
+
content: string,
|
|
4668
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4669
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
4670
|
+
const result = await this.mutateProjectManagementState(
|
|
4671
|
+
state,
|
|
4672
|
+
stored,
|
|
4673
|
+
writeContext,
|
|
4674
|
+
(snapshot) => {
|
|
4675
|
+
if (snapshot.scratchpad === content) return { changed: false, value: undefined };
|
|
4676
|
+
snapshot.scratchpad = content;
|
|
4677
|
+
return { changed: true, value: undefined };
|
|
4678
|
+
},
|
|
4679
|
+
);
|
|
4680
|
+
return this.projectScratchpadResult(result.snapshot);
|
|
4681
|
+
}
|
|
4682
|
+
|
|
4683
|
+
private async appendProjectScratchpadInState(
|
|
4684
|
+
state: IStorageState,
|
|
4685
|
+
stored: IStoredSessionState,
|
|
4686
|
+
content: string,
|
|
4687
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4688
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
4689
|
+
const result = await this.mutateProjectManagementState(
|
|
4690
|
+
state,
|
|
4691
|
+
stored,
|
|
4692
|
+
writeContext,
|
|
4693
|
+
(snapshot) => {
|
|
4694
|
+
if (content.length === 0) return { changed: false, value: undefined };
|
|
4695
|
+
const appended = `${snapshot.scratchpad}${content}`;
|
|
4696
|
+
validateUtf8String(
|
|
4697
|
+
appended,
|
|
4698
|
+
'project scratchpad',
|
|
4699
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
4700
|
+
);
|
|
4701
|
+
snapshot.scratchpad = appended;
|
|
4702
|
+
return { changed: true, value: undefined };
|
|
4703
|
+
},
|
|
4704
|
+
);
|
|
4705
|
+
return this.projectScratchpadResult(result.snapshot);
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4708
|
+
private async clearProjectScratchpadInState(
|
|
4709
|
+
state: IStorageState,
|
|
4710
|
+
stored: IStoredSessionState,
|
|
4711
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4712
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
4713
|
+
const result = await this.mutateProjectManagementState(
|
|
4714
|
+
state,
|
|
4715
|
+
stored,
|
|
4716
|
+
writeContext,
|
|
4717
|
+
(snapshot) => {
|
|
4718
|
+
if (snapshot.scratchpad.length === 0) return { changed: false, value: undefined };
|
|
4719
|
+
snapshot.scratchpad = '';
|
|
4720
|
+
return { changed: true, value: undefined };
|
|
4721
|
+
},
|
|
4722
|
+
);
|
|
4723
|
+
return this.projectScratchpadResult(result.snapshot);
|
|
4724
|
+
}
|
|
4725
|
+
|
|
4726
|
+
private async prepareGeneration(
|
|
4727
|
+
run: IActiveRun,
|
|
4728
|
+
options: IFlexPromptOptions,
|
|
4729
|
+
signal: AbortSignal,
|
|
4730
|
+
): Promise<plugins.IAgentGenerationLease> {
|
|
4731
|
+
signal.throwIfAborted();
|
|
4732
|
+
await this.prepareRunReversion(run, signal);
|
|
4733
|
+
run.phase = 'running';
|
|
4734
|
+
const queued = run.stored.outstandingPromptsById.get(run.queueId);
|
|
4735
|
+
if (queued && (queued.status === 'starting' || queued.status === 'scheduled')) {
|
|
4736
|
+
queued.status = 'running';
|
|
4737
|
+
this.emitPromptQueueEvent(queued, 'prompt.running');
|
|
4738
|
+
}
|
|
4739
|
+
const resolverRelationship = {
|
|
4740
|
+
...(run.stored.session.parentSessionId === undefined
|
|
4741
|
+
? {}
|
|
4742
|
+
: { parentSessionId: run.stored.session.parentSessionId }),
|
|
4743
|
+
...(run.stored.session.agent === undefined ? {} : { agent: run.stored.session.agent }),
|
|
4744
|
+
};
|
|
4745
|
+
const modelOutcome = Promise.resolve()
|
|
4746
|
+
.then(() => this.modelResolver.resolveModel(Object.freeze({
|
|
4747
|
+
scopeId: run.scopeId,
|
|
4748
|
+
scope: run.scope as TScope,
|
|
4749
|
+
sessionId: run.sessionId,
|
|
4750
|
+
runId: run.runId,
|
|
4751
|
+
...(options.modelHint ? { modelHint: options.modelHint } : {}),
|
|
3756
4752
|
...resolverRelationship,
|
|
3757
4753
|
signal,
|
|
3758
4754
|
})))
|
|
@@ -3839,17 +4835,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3839
4835
|
let tools: TFlexAgentToolSet | undefined;
|
|
3840
4836
|
try {
|
|
3841
4837
|
const providedTools = toolHandle?.tools;
|
|
3842
|
-
if (
|
|
3843
|
-
this.subagents.size > 0
|
|
3844
|
-
&& providedTools
|
|
3845
|
-
&& Object.prototype.hasOwnProperty.call(providedTools, 'task')
|
|
3846
|
-
) {
|
|
3847
|
-
throw new FlexHarnessValidationError('The application tool provider cannot define reserved tool "task".');
|
|
3848
|
-
}
|
|
3849
4838
|
const combinedTools: Record<string, unknown> = { ...(providedTools ?? {}) };
|
|
3850
|
-
|
|
3851
|
-
combinedTools.task = this.createSubagentTool(run);
|
|
3852
|
-
}
|
|
4839
|
+
Object.assign(combinedTools, this.createBuiltInTools(run));
|
|
3853
4840
|
tools = Object.keys(combinedTools).length > 0
|
|
3854
4841
|
? wrapToolSet(
|
|
3855
4842
|
combinedTools as TFlexAgentToolSet,
|
|
@@ -3900,6 +4887,283 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3900
4887
|
}
|
|
3901
4888
|
}
|
|
3902
4889
|
|
|
4890
|
+
private enabledBuiltInToolNames(run: IActiveRun): Set<string> {
|
|
4891
|
+
const names = new Set<string>();
|
|
4892
|
+
if (this.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
|
|
4893
|
+
names.add('delegate');
|
|
4894
|
+
}
|
|
4895
|
+
if (this.builtInTools.renameSession) names.add('rename_session');
|
|
4896
|
+
const projectManagement = this.builtInTools.projectManagement;
|
|
4897
|
+
if (projectManagement?.task) names.add('task');
|
|
4898
|
+
if (projectManagement?.goal) names.add('goal');
|
|
4899
|
+
if (projectManagement?.scratchpad) names.add('scratchpad');
|
|
4900
|
+
return names;
|
|
4901
|
+
}
|
|
4902
|
+
|
|
4903
|
+
private assertNoBuiltInToolCollisions(run: IActiveRun, tools: TFlexAgentToolSet): void {
|
|
4904
|
+
const reserved = this.enabledBuiltInToolNames(run);
|
|
4905
|
+
const collision = Object.keys(tools).find((name) => reserved.has(name));
|
|
4906
|
+
if (collision) {
|
|
4907
|
+
throw new FlexHarnessValidationError(
|
|
4908
|
+
`Tool provider cannot define enabled built-in tool "${collision}".`,
|
|
4909
|
+
);
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4913
|
+
private createBuiltInTools(run: IActiveRun): Record<string, unknown> {
|
|
4914
|
+
const tools: Record<string, unknown> = {};
|
|
4915
|
+
if (this.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
|
|
4916
|
+
tools.delegate = this.createSubagentTool(run);
|
|
4917
|
+
}
|
|
4918
|
+
if (this.builtInTools.renameSession) tools.rename_session = this.createRenameSessionTool(run);
|
|
4919
|
+
const projectManagement = this.builtInTools.projectManagement;
|
|
4920
|
+
if (projectManagement?.task) tools.task = this.createProjectTaskTool(run);
|
|
4921
|
+
if (projectManagement?.goal) tools.goal = this.createProjectGoalTool(run);
|
|
4922
|
+
if (projectManagement?.scratchpad) tools.scratchpad = this.createProjectScratchpadTool(run);
|
|
4923
|
+
return tools;
|
|
4924
|
+
}
|
|
4925
|
+
|
|
4926
|
+
private createRenameSessionTool(run: IActiveRun): unknown {
|
|
4927
|
+
return plugins.tool({
|
|
4928
|
+
description: 'Set the current FlexHarness session title.',
|
|
4929
|
+
inputSchema: plugins.z.object({
|
|
4930
|
+
title: plugins.z.string().min(1).max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxTitleBytes),
|
|
4931
|
+
}).strict(),
|
|
4932
|
+
execute: async ({ title }: { title: string }) => {
|
|
4933
|
+
validateUtf8String(
|
|
4934
|
+
title,
|
|
4935
|
+
'title',
|
|
4936
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTitleBytes,
|
|
4937
|
+
true,
|
|
4938
|
+
);
|
|
4939
|
+
let session!: IFlexSession;
|
|
4940
|
+
await this.mutateScope(run.state, () => {
|
|
4941
|
+
if (run.state.sessions.get(run.sessionId) !== run.stored) {
|
|
4942
|
+
throw new FlexHarnessNotFoundError('Session', run.sessionId);
|
|
4943
|
+
}
|
|
4944
|
+
run.stored.session.title = title;
|
|
4945
|
+
run.stored.session.updatedAt = new Date().toISOString();
|
|
4946
|
+
session = publicSnapshot(run.stored.session);
|
|
4947
|
+
});
|
|
4948
|
+
this.emitEvent(run.scopeId, run.sessionId, {
|
|
4949
|
+
type: 'session.updated',
|
|
4950
|
+
session,
|
|
4951
|
+
});
|
|
4952
|
+
return { session };
|
|
4953
|
+
},
|
|
4954
|
+
});
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4957
|
+
private createProjectTaskTool(run: IActiveRun): unknown {
|
|
4958
|
+
const status = plugins.z.enum(['pending', 'in_progress', 'completed', 'cancelled']);
|
|
4959
|
+
const priority = plugins.z.enum(['high', 'medium', 'low']);
|
|
4960
|
+
const id = plugins.z.string().min(1).max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskIdBytes);
|
|
4961
|
+
const content = plugins.z.string().min(1).max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskContentBytes);
|
|
4962
|
+
return plugins.tool({
|
|
4963
|
+
description: 'List, create, update, delete, or clear session-local project tasks.',
|
|
4964
|
+
inputSchema: plugins.z.discriminatedUnion('action', [
|
|
4965
|
+
plugins.z.object({ action: plugins.z.literal('list') }).strict(),
|
|
4966
|
+
plugins.z.object({
|
|
4967
|
+
action: plugins.z.literal('create'),
|
|
4968
|
+
id: id.optional(),
|
|
4969
|
+
content,
|
|
4970
|
+
status: status.optional(),
|
|
4971
|
+
priority: priority.optional(),
|
|
4972
|
+
}).strict(),
|
|
4973
|
+
plugins.z.object({
|
|
4974
|
+
action: plugins.z.literal('update'),
|
|
4975
|
+
id,
|
|
4976
|
+
content: content.optional(),
|
|
4977
|
+
status: status.optional(),
|
|
4978
|
+
priority: priority.optional(),
|
|
4979
|
+
}).strict().refine(
|
|
4980
|
+
(input) => input.content !== undefined
|
|
4981
|
+
|| input.status !== undefined
|
|
4982
|
+
|| input.priority !== undefined,
|
|
4983
|
+
{ message: 'Task update requires content, status, or priority.' },
|
|
4984
|
+
),
|
|
4985
|
+
plugins.z.object({ action: plugins.z.literal('delete'), id }).strict(),
|
|
4986
|
+
plugins.z.object({ action: plugins.z.literal('clear') }).strict(),
|
|
4987
|
+
]),
|
|
4988
|
+
execute: (
|
|
4989
|
+
input: TProjectTaskToolInput,
|
|
4990
|
+
options?: { toolCallId?: string },
|
|
4991
|
+
) => this.executeProjectTaskTool(run, input, options?.toolCallId),
|
|
4992
|
+
});
|
|
4993
|
+
}
|
|
4994
|
+
|
|
4995
|
+
private createProjectGoalTool(run: IActiveRun): unknown {
|
|
4996
|
+
return plugins.tool({
|
|
4997
|
+
description: 'Get, set, or clear the session-local project goal.',
|
|
4998
|
+
inputSchema: plugins.z.discriminatedUnion('action', [
|
|
4999
|
+
plugins.z.object({ action: plugins.z.literal('get') }).strict(),
|
|
5000
|
+
plugins.z.object({
|
|
5001
|
+
action: plugins.z.literal('set'),
|
|
5002
|
+
goal: plugins.z.string().min(1).max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxGoalBytes),
|
|
5003
|
+
}).strict(),
|
|
5004
|
+
plugins.z.object({ action: plugins.z.literal('clear') }).strict(),
|
|
5005
|
+
]),
|
|
5006
|
+
execute: (
|
|
5007
|
+
input: TProjectGoalToolInput,
|
|
5008
|
+
options?: { toolCallId?: string },
|
|
5009
|
+
) => this.executeProjectGoalTool(run, input, options?.toolCallId),
|
|
5010
|
+
});
|
|
5011
|
+
}
|
|
5012
|
+
|
|
5013
|
+
private createProjectScratchpadTool(run: IActiveRun): unknown {
|
|
5014
|
+
const content = plugins.z.string().max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes);
|
|
5015
|
+
return plugins.tool({
|
|
5016
|
+
description: 'Get, set, append to, or clear the session-local project scratchpad.',
|
|
5017
|
+
inputSchema: plugins.z.discriminatedUnion('action', [
|
|
5018
|
+
plugins.z.object({ action: plugins.z.literal('get') }).strict(),
|
|
5019
|
+
plugins.z.object({ action: plugins.z.literal('set'), content }).strict(),
|
|
5020
|
+
plugins.z.object({ action: plugins.z.literal('append'), content }).strict(),
|
|
5021
|
+
plugins.z.object({ action: plugins.z.literal('clear') }).strict(),
|
|
5022
|
+
]),
|
|
5023
|
+
execute: (
|
|
5024
|
+
input: TProjectScratchpadToolInput,
|
|
5025
|
+
options?: { toolCallId?: string },
|
|
5026
|
+
) => this.executeProjectScratchpadTool(run, input, options?.toolCallId),
|
|
5027
|
+
});
|
|
5028
|
+
}
|
|
5029
|
+
|
|
5030
|
+
private projectManagementAgentWriteContext(
|
|
5031
|
+
run: IActiveRun,
|
|
5032
|
+
toolCallId: string | undefined,
|
|
5033
|
+
): IFlexProjectManagementWriteContext {
|
|
5034
|
+
return Object.freeze({
|
|
5035
|
+
actor: 'agent' as const,
|
|
5036
|
+
runId: run.runId,
|
|
5037
|
+
...(run.stored.session.agent === undefined ? {} : { agent: run.stored.session.agent }),
|
|
5038
|
+
...(toolCallId === undefined ? {} : { toolCallId }),
|
|
5039
|
+
});
|
|
5040
|
+
}
|
|
5041
|
+
|
|
5042
|
+
private async executeProjectTaskTool(
|
|
5043
|
+
run: IActiveRun,
|
|
5044
|
+
input: TProjectTaskToolInput,
|
|
5045
|
+
toolCallId: string | undefined,
|
|
5046
|
+
): Promise<IFlexProjectStateResult | IFlexProjectTaskResult | IFlexProjectTasksResult> {
|
|
5047
|
+
this.assertProjectManagementSession(run.state, run.stored);
|
|
5048
|
+
if (input.action === 'list') {
|
|
5049
|
+
const snapshot = await this.readProjectManagementState(run.state, run.stored);
|
|
5050
|
+
return this.projectTasksResult(snapshot, snapshot.tasks);
|
|
5051
|
+
}
|
|
5052
|
+
const writeContext = this.projectManagementAgentWriteContext(run, toolCallId);
|
|
5053
|
+
if (input.action === 'create') {
|
|
5054
|
+
const id = input.id ?? (() => {
|
|
5055
|
+
validateUtf8String(
|
|
5056
|
+
toolCallId,
|
|
5057
|
+
'task toolCallId',
|
|
5058
|
+
maxTransferIdentifierBytes,
|
|
5059
|
+
true,
|
|
5060
|
+
);
|
|
5061
|
+
return this.createProjectTaskId(
|
|
5062
|
+
run.state.storageKey,
|
|
5063
|
+
run.sessionId,
|
|
5064
|
+
run.runId,
|
|
5065
|
+
toolCallId,
|
|
5066
|
+
);
|
|
5067
|
+
})();
|
|
5068
|
+
const createInput: IFlexCreateProjectTaskInput = {
|
|
5069
|
+
id,
|
|
5070
|
+
content: input.content,
|
|
5071
|
+
...(input.status === undefined ? {} : { status: input.status }),
|
|
5072
|
+
...(input.priority === undefined ? {} : { priority: input.priority }),
|
|
5073
|
+
};
|
|
5074
|
+
validateCreateProjectTaskInput(createInput);
|
|
5075
|
+
return this.createProjectTaskInState(run.state, run.stored, createInput, writeContext);
|
|
5076
|
+
}
|
|
5077
|
+
if (input.action === 'update') {
|
|
5078
|
+
const updateInput: IFlexUpdateProjectTaskInput = {
|
|
5079
|
+
id: input.id,
|
|
5080
|
+
...(input.content === undefined ? {} : { content: input.content }),
|
|
5081
|
+
...(input.status === undefined ? {} : { status: input.status }),
|
|
5082
|
+
...(input.priority === undefined ? {} : { priority: input.priority }),
|
|
5083
|
+
};
|
|
5084
|
+
validateUpdateProjectTaskInput(updateInput);
|
|
5085
|
+
return this.updateProjectTaskInState(run.state, run.stored, updateInput, writeContext);
|
|
5086
|
+
}
|
|
5087
|
+
if (input.action === 'delete') {
|
|
5088
|
+
validateProjectTaskId(input.id);
|
|
5089
|
+
return this.deleteProjectTaskInState(run.state, run.stored, input.id, writeContext);
|
|
5090
|
+
}
|
|
5091
|
+
return this.clearProjectTasksInState(run.state, run.stored, writeContext);
|
|
5092
|
+
}
|
|
5093
|
+
|
|
5094
|
+
private async executeProjectGoalTool(
|
|
5095
|
+
run: IActiveRun,
|
|
5096
|
+
input: TProjectGoalToolInput,
|
|
5097
|
+
toolCallId: string | undefined,
|
|
5098
|
+
): Promise<IFlexProjectGoalResult> {
|
|
5099
|
+
this.assertProjectManagementSession(run.state, run.stored);
|
|
5100
|
+
if (input.action === 'get') {
|
|
5101
|
+
return this.projectGoalResult(await this.readProjectManagementState(run.state, run.stored));
|
|
5102
|
+
}
|
|
5103
|
+
const writeContext = this.projectManagementAgentWriteContext(run, toolCallId);
|
|
5104
|
+
if (input.action === 'set') {
|
|
5105
|
+
validateUtf8String(
|
|
5106
|
+
input.goal,
|
|
5107
|
+
'project goal',
|
|
5108
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxGoalBytes,
|
|
5109
|
+
true,
|
|
5110
|
+
);
|
|
5111
|
+
return this.setProjectGoalInState(run.state, run.stored, input.goal, writeContext);
|
|
5112
|
+
}
|
|
5113
|
+
return this.clearProjectGoalInState(run.state, run.stored, writeContext);
|
|
5114
|
+
}
|
|
5115
|
+
|
|
5116
|
+
private async executeProjectScratchpadTool(
|
|
5117
|
+
run: IActiveRun,
|
|
5118
|
+
input: TProjectScratchpadToolInput,
|
|
5119
|
+
toolCallId: string | undefined,
|
|
5120
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
5121
|
+
this.assertProjectManagementSession(run.state, run.stored);
|
|
5122
|
+
if (input.action === 'get') {
|
|
5123
|
+
return this.projectScratchpadResult(
|
|
5124
|
+
await this.readProjectManagementState(run.state, run.stored),
|
|
5125
|
+
);
|
|
5126
|
+
}
|
|
5127
|
+
const writeContext = this.projectManagementAgentWriteContext(run, toolCallId);
|
|
5128
|
+
if (input.action === 'set') {
|
|
5129
|
+
validateUtf8String(
|
|
5130
|
+
input.content,
|
|
5131
|
+
'project scratchpad',
|
|
5132
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
5133
|
+
);
|
|
5134
|
+
return this.setProjectScratchpadInState(run.state, run.stored, input.content, writeContext);
|
|
5135
|
+
}
|
|
5136
|
+
if (input.action === 'append') {
|
|
5137
|
+
validateUtf8String(
|
|
5138
|
+
input.content,
|
|
5139
|
+
'project scratchpad append',
|
|
5140
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
5141
|
+
);
|
|
5142
|
+
return this.appendProjectScratchpadInState(
|
|
5143
|
+
run.state,
|
|
5144
|
+
run.stored,
|
|
5145
|
+
input.content,
|
|
5146
|
+
writeContext,
|
|
5147
|
+
);
|
|
5148
|
+
}
|
|
5149
|
+
return this.clearProjectScratchpadInState(run.state, run.stored, writeContext);
|
|
5150
|
+
}
|
|
5151
|
+
|
|
5152
|
+
private createProjectTaskId(
|
|
5153
|
+
storageKey: string,
|
|
5154
|
+
sessionId: string,
|
|
5155
|
+
runId: string,
|
|
5156
|
+
toolCallId: string,
|
|
5157
|
+
): string {
|
|
5158
|
+
return `task_${sha256Hex(JSON.stringify([
|
|
5159
|
+
'flexharness-project-task-v1',
|
|
5160
|
+
storageKey,
|
|
5161
|
+
sessionId,
|
|
5162
|
+
runId,
|
|
5163
|
+
toolCallId,
|
|
5164
|
+
]))}`;
|
|
5165
|
+
}
|
|
5166
|
+
|
|
3903
5167
|
private createSubagentTool(run: IActiveRun): unknown {
|
|
3904
5168
|
const available = [...this.subagents.values()]
|
|
3905
5169
|
.map((definition) => `- ${definition.name}: ${definition.description}`)
|
|
@@ -3930,16 +5194,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3930
5194
|
}
|
|
3931
5195
|
validateUtf8String(
|
|
3932
5196
|
input.description,
|
|
3933
|
-
'
|
|
5197
|
+
'delegate description',
|
|
3934
5198
|
maxSubagentTaskDescriptionBytes,
|
|
3935
5199
|
true,
|
|
3936
5200
|
);
|
|
3937
|
-
validateUtf8String(input.prompt, '
|
|
5201
|
+
validateUtf8String(input.prompt, 'delegate prompt', maxSubagentPromptBytes, true);
|
|
3938
5202
|
validateUtf8String(input.subagentType, 'subagentType', maxSubagentNameBytes, true);
|
|
3939
5203
|
if (input.taskId !== undefined) {
|
|
3940
5204
|
validateUtf8String(input.taskId, 'taskId', maxSubagentTaskIdBytes, true);
|
|
3941
5205
|
}
|
|
3942
|
-
validateUtf8String(toolCallId, '
|
|
5206
|
+
validateUtf8String(toolCallId, 'delegate toolCallId', maxTransferIdentifierBytes, true);
|
|
3943
5207
|
const definition = this.subagents.get(input.subagentType);
|
|
3944
5208
|
if (!definition || (run.stored.session.depth ?? 0) >= this.maxSubagentDepth) {
|
|
3945
5209
|
throw new FlexHarnessValidationError(`Subagent "${input.subagentType}" is not available.`);
|
|
@@ -4163,6 +5427,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4163
5427
|
metadata = {
|
|
4164
5428
|
scopeId: run.scopeId,
|
|
4165
5429
|
sessionId,
|
|
5430
|
+
sessionGenerationId: createSessionGenerationId(),
|
|
5431
|
+
sessionGenerationSequence: state.revision + 1,
|
|
4166
5432
|
title: `Subagent: ${definition.name}`,
|
|
4167
5433
|
createdAt: timestamp,
|
|
4168
5434
|
updatedAt: timestamp,
|
|
@@ -4236,6 +5502,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4236
5502
|
state.sessions.delete(sessionId);
|
|
4237
5503
|
state.tombstones.set(sessionId, {
|
|
4238
5504
|
sessionId,
|
|
5505
|
+
...requireSessionGeneration(metadata!),
|
|
4239
5506
|
deletedAt: new Date().toISOString(),
|
|
4240
5507
|
rootSessionId: sessionId,
|
|
4241
5508
|
depth: 0,
|
|
@@ -4291,12 +5558,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4291
5558
|
const partId = run.toolPartIds.get(toolCallId);
|
|
4292
5559
|
const part = run.callbackParts.find((entry) => entry.partId === partId && entry.type === 'tool');
|
|
4293
5560
|
if (!part || part.type !== 'tool' || part.status !== 'running') {
|
|
4294
|
-
throw new FlexHarnessValidationError(`Running
|
|
5561
|
+
throw new FlexHarnessValidationError(`Running delegate part "${toolCallId}" is unavailable.`);
|
|
4295
5562
|
}
|
|
4296
5563
|
const bytes = Buffer.byteLength(childSessionId, 'utf8')
|
|
4297
5564
|
+ (model === undefined ? 0 : jsonBytes(model));
|
|
4298
5565
|
if (!this.reserveCallbackCapacity(run, 1, bytes, 0)) {
|
|
4299
|
-
throw run.callbackError ?? new FlexHarnessCallbackOverflowError('
|
|
5566
|
+
throw run.callbackError ?? new FlexHarnessCallbackOverflowError('Delegate metadata exceeded callback limits.');
|
|
4300
5567
|
}
|
|
4301
5568
|
part.childSessionId = childSessionId;
|
|
4302
5569
|
if (model !== undefined) part.model = cloneSerializable(model);
|
|
@@ -4445,6 +5712,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4445
5712
|
return `${kind}_${sha256Hex(JSON.stringify([storageKey, sessionId, value]))}`;
|
|
4446
5713
|
}
|
|
4447
5714
|
|
|
5715
|
+
private reversionProviderProtocolVersion(): 1 | 2 | undefined {
|
|
5716
|
+
if (!this.turnReversionProvider) return undefined;
|
|
5717
|
+
return 'protocolVersion' in this.turnReversionProvider ? 2 : 1;
|
|
5718
|
+
}
|
|
5719
|
+
|
|
4448
5720
|
private reversionCaptureContext(
|
|
4449
5721
|
run: IActiveRun,
|
|
4450
5722
|
captureId: string,
|
|
@@ -4491,9 +5763,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4491
5763
|
sessionId: string,
|
|
4492
5764
|
runId: string,
|
|
4493
5765
|
captureId: string,
|
|
4494
|
-
) {
|
|
4495
|
-
return this.withReversionMaintenanceSignal((signal) =>
|
|
4496
|
-
this.turnReversionProvider
|
|
5766
|
+
): Promise<TNormalizedCaptureInspection> {
|
|
5767
|
+
return this.withReversionMaintenanceSignal(async (signal) => {
|
|
5768
|
+
const provider = this.turnReversionProvider!;
|
|
5769
|
+
const inspection = await provider.inspectCapture(Object.freeze({
|
|
4497
5770
|
scopeId,
|
|
4498
5771
|
scope,
|
|
4499
5772
|
storageKey,
|
|
@@ -4501,7 +5774,29 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4501
5774
|
runId,
|
|
4502
5775
|
captureId,
|
|
4503
5776
|
signal,
|
|
4504
|
-
}))
|
|
5777
|
+
}));
|
|
5778
|
+
if (inspection.status !== 'finalized') return { status: inspection.status };
|
|
5779
|
+
if (this.reversionProviderProtocolVersion() === 2) {
|
|
5780
|
+
if (!('outcome' in inspection)) {
|
|
5781
|
+
throw new FlexHarnessValidationError('Protocol-2 finalized inspection has no outcome.');
|
|
5782
|
+
}
|
|
5783
|
+
return {
|
|
5784
|
+
status: 'finalized',
|
|
5785
|
+
outcome: this.normalizeReversionOutcome(inspection.outcome),
|
|
5786
|
+
};
|
|
5787
|
+
}
|
|
5788
|
+
if (!('reference' in inspection) || inspection.reference === undefined) {
|
|
5789
|
+
throw new FlexHarnessValidationError('Protocol-1 finalized inspection has no reference.');
|
|
5790
|
+
}
|
|
5791
|
+
return {
|
|
5792
|
+
status: 'finalized',
|
|
5793
|
+
outcome: {
|
|
5794
|
+
disposition: 'revertible',
|
|
5795
|
+
reference: this.normalizeReversionReference(inspection.reference),
|
|
5796
|
+
affectedWorkspaces: [],
|
|
5797
|
+
},
|
|
5798
|
+
};
|
|
5799
|
+
});
|
|
4505
5800
|
}
|
|
4506
5801
|
|
|
4507
5802
|
private finalizeReversionCapture(
|
|
@@ -4511,9 +5806,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4511
5806
|
sessionId: string,
|
|
4512
5807
|
runId: string,
|
|
4513
5808
|
captureId: string,
|
|
4514
|
-
) {
|
|
4515
|
-
return this.withReversionMaintenanceSignal((signal) =>
|
|
4516
|
-
this.turnReversionProvider!.finalize(Object.freeze({
|
|
5809
|
+
): Promise<TFlexTurnReversionFinalizedOutcome> {
|
|
5810
|
+
return this.withReversionMaintenanceSignal(async (signal) => {
|
|
5811
|
+
const result = await this.turnReversionProvider!.finalize(Object.freeze({
|
|
4517
5812
|
scopeId,
|
|
4518
5813
|
scope,
|
|
4519
5814
|
storageKey,
|
|
@@ -4521,10 +5816,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4521
5816
|
runId,
|
|
4522
5817
|
captureId,
|
|
4523
5818
|
signal,
|
|
4524
|
-
}))
|
|
5819
|
+
}));
|
|
5820
|
+
return this.reversionProviderProtocolVersion() === 2
|
|
5821
|
+
? this.normalizeReversionOutcome(result)
|
|
5822
|
+
: {
|
|
5823
|
+
disposition: 'revertible',
|
|
5824
|
+
reference: this.normalizeReversionReference(result),
|
|
5825
|
+
affectedWorkspaces: [],
|
|
5826
|
+
};
|
|
5827
|
+
});
|
|
4525
5828
|
}
|
|
4526
5829
|
|
|
4527
|
-
private async
|
|
5830
|
+
private async resolveReversionCaptureOutcome(
|
|
4528
5831
|
state: IStorageState,
|
|
4529
5832
|
scopeId: string,
|
|
4530
5833
|
scope: TScope,
|
|
@@ -4532,7 +5835,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4532
5835
|
runId: string,
|
|
4533
5836
|
captureId: string,
|
|
4534
5837
|
durableState: 'preparing' | 'prepared' | 'finalizing',
|
|
4535
|
-
): Promise<
|
|
5838
|
+
): Promise<TFlexTurnReversionFinalizedOutcome | undefined> {
|
|
4536
5839
|
let inspection;
|
|
4537
5840
|
try {
|
|
4538
5841
|
inspection = await this.inspectReversionCapture(
|
|
@@ -4556,10 +5859,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4556
5859
|
state.lifecycle = 'fenced';
|
|
4557
5860
|
throw new FlexHarnessValidationError('Prepared reversion capture is missing.');
|
|
4558
5861
|
}
|
|
4559
|
-
let
|
|
5862
|
+
let outcome = inspection.status === 'finalized' ? inspection.outcome : undefined;
|
|
4560
5863
|
if (inspection.status === 'prepared') {
|
|
4561
5864
|
try {
|
|
4562
|
-
|
|
5865
|
+
outcome = await this.finalizeReversionCapture(
|
|
4563
5866
|
scopeId,
|
|
4564
5867
|
scope,
|
|
4565
5868
|
state.storageKey,
|
|
@@ -4576,27 +5879,123 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4576
5879
|
runId,
|
|
4577
5880
|
captureId,
|
|
4578
5881
|
);
|
|
4579
|
-
if (finalInspection.status !== 'finalized'
|
|
5882
|
+
if (finalInspection.status !== 'finalized') {
|
|
4580
5883
|
if (finalInspection.status === 'unknown') state.lifecycle = 'fenced';
|
|
4581
5884
|
throw this.projectOperationError(error, 'turnReversion', scopeId, sessionId, captureId);
|
|
4582
5885
|
}
|
|
4583
|
-
|
|
5886
|
+
outcome = finalInspection.outcome;
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
if (outcome === undefined) {
|
|
5890
|
+
state.lifecycle = 'fenced';
|
|
5891
|
+
throw new FlexHarnessValidationError('Finalized reversion capture has no outcome.');
|
|
5892
|
+
}
|
|
5893
|
+
return outcome;
|
|
5894
|
+
}
|
|
5895
|
+
|
|
5896
|
+
private normalizeReversionReference(reference: unknown): TJsonValue {
|
|
5897
|
+
return normalizeJsonValue(reference, {
|
|
5898
|
+
maxDepth: this.toolOutputLimits.maxDepth,
|
|
5899
|
+
maxBytes: Math.min(
|
|
5900
|
+
this.toolOutputLimits.maxBytes,
|
|
5901
|
+
FLEX_REVERSION_REFERENCE_MAX_BYTES,
|
|
5902
|
+
),
|
|
5903
|
+
});
|
|
5904
|
+
}
|
|
5905
|
+
|
|
5906
|
+
private normalizeReversionOutcome(value: unknown): TFlexTurnReversionFinalizedOutcome {
|
|
5907
|
+
if (
|
|
5908
|
+
!value
|
|
5909
|
+
|| typeof value !== 'object'
|
|
5910
|
+
|| Array.isArray(value)
|
|
5911
|
+
|| (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
5912
|
+
) throw new FlexHarnessValidationError('Turn reversion finalized outcome must be a plain object.');
|
|
5913
|
+
const outcome = value as Record<string, unknown>;
|
|
5914
|
+
const disposition = outcome.disposition;
|
|
5915
|
+
if (!['revertible', 'no-change', 'nonrevertible'].includes(String(disposition))) {
|
|
5916
|
+
throw new FlexHarnessValidationError('Turn reversion finalized outcome disposition is invalid.');
|
|
5917
|
+
}
|
|
5918
|
+
const allowedKeys = disposition === 'nonrevertible'
|
|
5919
|
+
? ['disposition', 'reference', 'reasonCode', 'affectedWorkspaces']
|
|
5920
|
+
: ['disposition', 'reference', 'affectedWorkspaces'];
|
|
5921
|
+
const unsupported = Object.keys(outcome).find((key) => !allowedKeys.includes(key));
|
|
5922
|
+
if (unsupported) {
|
|
5923
|
+
throw new FlexHarnessValidationError(
|
|
5924
|
+
`Turn reversion finalized outcome does not support "${unsupported}".`,
|
|
5925
|
+
);
|
|
5926
|
+
}
|
|
5927
|
+
if (!Object.prototype.hasOwnProperty.call(outcome, 'reference')) {
|
|
5928
|
+
throw new FlexHarnessValidationError('Turn reversion finalized outcome requires a reference.');
|
|
5929
|
+
}
|
|
5930
|
+
const reference = this.normalizeReversionReference(outcome.reference);
|
|
5931
|
+
const affectedWorkspaces = outcome.affectedWorkspaces === undefined
|
|
5932
|
+
? undefined
|
|
5933
|
+
: this.normalizeAffectedWorkspaces(outcome.affectedWorkspaces);
|
|
5934
|
+
if (disposition === 'revertible') {
|
|
5935
|
+
if (affectedWorkspaces === undefined) {
|
|
5936
|
+
throw new FlexHarnessValidationError('Revertible outcome requires affectedWorkspaces.');
|
|
4584
5937
|
}
|
|
5938
|
+
return { disposition, reference, affectedWorkspaces };
|
|
4585
5939
|
}
|
|
4586
|
-
if (
|
|
4587
|
-
|
|
4588
|
-
|
|
5940
|
+
if (disposition === 'no-change') {
|
|
5941
|
+
if (affectedWorkspaces && affectedWorkspaces.length > 0) {
|
|
5942
|
+
throw new FlexHarnessValidationError('No-change outcome cannot affect a workspace.');
|
|
5943
|
+
}
|
|
5944
|
+
return {
|
|
5945
|
+
disposition,
|
|
5946
|
+
reference,
|
|
5947
|
+
...(affectedWorkspaces === undefined ? {} : { affectedWorkspaces: [] }),
|
|
5948
|
+
};
|
|
4589
5949
|
}
|
|
4590
|
-
|
|
5950
|
+
if (
|
|
5951
|
+
typeof outcome.reasonCode !== 'string'
|
|
5952
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(outcome.reasonCode)
|
|
5953
|
+
|| Buffer.byteLength(outcome.reasonCode, 'utf8') > FLEX_REVERSION_REASON_CODE_MAX_BYTES
|
|
5954
|
+
) throw new FlexHarnessValidationError('Nonrevertible outcome reasonCode is invalid.');
|
|
5955
|
+
return {
|
|
5956
|
+
disposition: 'nonrevertible',
|
|
5957
|
+
reference,
|
|
5958
|
+
reasonCode: outcome.reasonCode,
|
|
5959
|
+
...(affectedWorkspaces === undefined ? {} : { affectedWorkspaces }),
|
|
5960
|
+
};
|
|
4591
5961
|
}
|
|
4592
5962
|
|
|
4593
|
-
private
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
5963
|
+
private normalizeAffectedWorkspaces(value: unknown): IFlexAffectedWorkspace[] {
|
|
5964
|
+
if (!Array.isArray(value) || value.length > FLEX_REVERSION_MAX_AFFECTED_WORKSPACES) {
|
|
5965
|
+
throw new FlexHarnessValidationError(
|
|
5966
|
+
`affectedWorkspaces must contain at most ${FLEX_REVERSION_MAX_AFFECTED_WORKSPACES} entries.`,
|
|
5967
|
+
);
|
|
5968
|
+
}
|
|
5969
|
+
const ids = new Set<string>();
|
|
5970
|
+
return value.map((entry, index) => {
|
|
5971
|
+
if (
|
|
5972
|
+
!entry
|
|
5973
|
+
|| typeof entry !== 'object'
|
|
5974
|
+
|| Array.isArray(entry)
|
|
5975
|
+
|| (Object.getPrototypeOf(entry) !== Object.prototype && Object.getPrototypeOf(entry) !== null)
|
|
5976
|
+
) throw new FlexHarnessValidationError(`affectedWorkspaces[${index}] must be a plain object.`);
|
|
5977
|
+
const descriptor = entry as Record<string, unknown>;
|
|
5978
|
+
const unsupported = Object.keys(descriptor).find((key) => !['id', 'label'].includes(key));
|
|
5979
|
+
if (unsupported) {
|
|
5980
|
+
throw new FlexHarnessValidationError(
|
|
5981
|
+
`affectedWorkspaces[${index}] does not support "${unsupported}".`,
|
|
5982
|
+
);
|
|
5983
|
+
}
|
|
5984
|
+
if (
|
|
5985
|
+
typeof descriptor.id !== 'string'
|
|
5986
|
+
|| !descriptor.id.trim()
|
|
5987
|
+
|| Buffer.byteLength(descriptor.id, 'utf8') > FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES
|
|
5988
|
+
) throw new FlexHarnessValidationError(`affectedWorkspaces[${index}].id is invalid.`);
|
|
5989
|
+
if (
|
|
5990
|
+
typeof descriptor.label !== 'string'
|
|
5991
|
+
|| !descriptor.label.trim()
|
|
5992
|
+
|| Buffer.byteLength(descriptor.label, 'utf8') > FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES
|
|
5993
|
+
) throw new FlexHarnessValidationError(`affectedWorkspaces[${index}].label is invalid.`);
|
|
5994
|
+
if (ids.has(descriptor.id)) {
|
|
5995
|
+
throw new FlexHarnessValidationError(`affectedWorkspaces contains duplicate id "${descriptor.id}".`);
|
|
5996
|
+
}
|
|
5997
|
+
ids.add(descriptor.id);
|
|
5998
|
+
return { id: descriptor.id, label: descriptor.label };
|
|
4600
5999
|
});
|
|
4601
6000
|
}
|
|
4602
6001
|
|
|
@@ -4612,6 +6011,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4612
6011
|
);
|
|
4613
6012
|
}
|
|
4614
6013
|
const context = this.reversionCaptureContext(run, captureId, signal);
|
|
6014
|
+
if (this.reversionProviderProtocolVersion() !== segment.protocolVersion) {
|
|
6015
|
+
throw this.projectExternalError(
|
|
6016
|
+
run,
|
|
6017
|
+
new Error(`Root generation requires reversion protocolVersion ${segment.protocolVersion}.`),
|
|
6018
|
+
'turnReversion',
|
|
6019
|
+
);
|
|
6020
|
+
}
|
|
4615
6021
|
try {
|
|
4616
6022
|
await this.turnReversionProvider.prepare(context);
|
|
4617
6023
|
} catch (error) {
|
|
@@ -4653,7 +6059,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4653
6059
|
const eventIds = run.stored.agentSession.getEvents()
|
|
4654
6060
|
.filter((event) => event.generationId === run.runId)
|
|
4655
6061
|
.map((event) => event.id);
|
|
4656
|
-
if (!this.turnReversionProvider ||
|
|
6062
|
+
if (!this.turnReversionProvider || segment.provenance === 'transcript') {
|
|
4657
6063
|
await this.mutateProjection(run.state, run.stored, () => {
|
|
4658
6064
|
const wasCompleted = segment.status === 'completed';
|
|
4659
6065
|
segment.status = status;
|
|
@@ -4665,6 +6071,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4665
6071
|
}, true);
|
|
4666
6072
|
return;
|
|
4667
6073
|
}
|
|
6074
|
+
if (!segment.captureId) {
|
|
6075
|
+
throw new FlexHarnessValidationError('Workspace reversion segment has no capture ownership.');
|
|
6076
|
+
}
|
|
4668
6077
|
const captureId = segment.captureId;
|
|
4669
6078
|
if (segment.workspaceReference !== undefined) {
|
|
4670
6079
|
await this.mutateProjection(run.state, run.stored, () => {
|
|
@@ -4681,16 +6090,30 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4681
6090
|
if (pending?.kind !== 'capture' || pending.runId !== run.runId) {
|
|
4682
6091
|
throw new FlexHarnessValidationError('Reversion capture intent changed before finalization.');
|
|
4683
6092
|
}
|
|
4684
|
-
|
|
6093
|
+
if (this.reversionProviderProtocolVersion() !== pending.protocolVersion) {
|
|
6094
|
+
throw new FlexHarnessValidationError(
|
|
6095
|
+
`Reversion capture finalization requires protocolVersion ${pending.protocolVersion}.`,
|
|
6096
|
+
);
|
|
6097
|
+
}
|
|
6098
|
+
if (pending.state === 'prepared') {
|
|
6099
|
+
await this.mutateProjection(run.state, run.stored, () => {
|
|
6100
|
+
const current = run.stored.pendingReversion;
|
|
6101
|
+
if (current?.kind !== 'capture' || current.captureId !== captureId) {
|
|
6102
|
+
throw new FlexHarnessValidationError('Reversion capture changed before finalization.');
|
|
6103
|
+
}
|
|
6104
|
+
current.state = 'finalizing';
|
|
6105
|
+
}, true);
|
|
6106
|
+
}
|
|
6107
|
+
const outcome = await this.resolveReversionCaptureOutcome(
|
|
4685
6108
|
run.state,
|
|
4686
6109
|
run.scopeId,
|
|
4687
6110
|
run.scope as TScope,
|
|
4688
6111
|
run.sessionId,
|
|
4689
6112
|
run.runId,
|
|
4690
6113
|
captureId,
|
|
4691
|
-
pending.state,
|
|
6114
|
+
pending.state === 'prepared' ? 'finalizing' : pending.state,
|
|
4692
6115
|
);
|
|
4693
|
-
if (
|
|
6116
|
+
if (outcome === undefined) {
|
|
4694
6117
|
await this.mutateProjection(run.state, run.stored, () => {
|
|
4695
6118
|
run.stored.reversionSegments = run.stored.reversionSegments.filter(
|
|
4696
6119
|
(entry) => entry.runId !== run.runId,
|
|
@@ -4707,7 +6130,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4707
6130
|
const wasCompleted = segment.status === 'completed';
|
|
4708
6131
|
segment.status = status;
|
|
4709
6132
|
segment.eventIds = eventIds;
|
|
4710
|
-
|
|
6133
|
+
this.applyFinalizedReversionOutcome(run.stored, segment, outcome);
|
|
4711
6134
|
if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
|
|
4712
6135
|
else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
|
|
4713
6136
|
delete run.stored.pendingReversion;
|
|
@@ -4715,34 +6138,73 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4715
6138
|
}, true);
|
|
4716
6139
|
}
|
|
4717
6140
|
|
|
6141
|
+
private applyFinalizedReversionOutcome(
|
|
6142
|
+
stored: IStoredSessionState,
|
|
6143
|
+
segment: IFlexReversionSegment,
|
|
6144
|
+
outcome: TFlexTurnReversionFinalizedOutcome,
|
|
6145
|
+
): void {
|
|
6146
|
+
const captureId = segment.captureId;
|
|
6147
|
+
if (!captureId) {
|
|
6148
|
+
throw new FlexHarnessValidationError('Finalized workspace outcome has no capture ownership.');
|
|
6149
|
+
}
|
|
6150
|
+
segment.disposition = outcome.disposition;
|
|
6151
|
+
delete segment.affectedWorkspaces;
|
|
6152
|
+
delete segment.reasonCode;
|
|
6153
|
+
if (segment.protocolVersion === 2) {
|
|
6154
|
+
if (outcome.affectedWorkspaces !== undefined) {
|
|
6155
|
+
segment.affectedWorkspaces = cloneSerializable([...outcome.affectedWorkspaces]);
|
|
6156
|
+
}
|
|
6157
|
+
if (outcome.disposition === 'nonrevertible') segment.reasonCode = outcome.reasonCode;
|
|
6158
|
+
}
|
|
6159
|
+
if (outcome.disposition === 'revertible') {
|
|
6160
|
+
segment.workspaceReference = cloneSerializable(outcome.reference);
|
|
6161
|
+
return;
|
|
6162
|
+
}
|
|
6163
|
+
this.enqueueReversionReleaseReference(stored, {
|
|
6164
|
+
runId: segment.runId,
|
|
6165
|
+
captureId,
|
|
6166
|
+
reference: cloneSerializable(outcome.reference),
|
|
6167
|
+
protocolVersion: segment.protocolVersion,
|
|
6168
|
+
});
|
|
6169
|
+
delete segment.captureId;
|
|
6170
|
+
delete segment.workspaceReference;
|
|
6171
|
+
}
|
|
6172
|
+
|
|
4718
6173
|
private hiddenReversionSegments(stored: IStoredSessionState): IFlexReversionSegment[] {
|
|
4719
|
-
const
|
|
4720
|
-
const firstHidden = candidates[stored.revertCursor];
|
|
6174
|
+
const firstHidden = this.reversionGroups(stored)[stored.revertCursor]?.segments[0];
|
|
4721
6175
|
if (!firstHidden) return [];
|
|
4722
6176
|
const index = stored.reversionSegments.findIndex((segment) => segment.runId === firstHidden.runId);
|
|
4723
6177
|
if (index < 0) return [];
|
|
4724
|
-
return stored.reversionSegments.slice(
|
|
6178
|
+
return stored.reversionSegments.slice(index);
|
|
4725
6179
|
}
|
|
4726
6180
|
|
|
4727
6181
|
private enqueueReversionRelease(
|
|
4728
6182
|
stored: IStoredSessionState,
|
|
4729
6183
|
segment: IFlexReversionSegment,
|
|
4730
6184
|
): void {
|
|
4731
|
-
if (
|
|
6185
|
+
if (segment.disposition !== 'revertible') return;
|
|
4732
6186
|
if (segment.captureId === undefined || segment.workspaceReference === undefined) {
|
|
4733
6187
|
throw new FlexHarnessValidationError('Capture-backed segment has no releasable workspace reference.');
|
|
4734
6188
|
}
|
|
4735
|
-
|
|
6189
|
+
this.enqueueReversionReleaseReference(stored, {
|
|
6190
|
+
runId: segment.runId,
|
|
6191
|
+
captureId: segment.captureId,
|
|
6192
|
+
reference: cloneSerializable(segment.workspaceReference),
|
|
6193
|
+
protocolVersion: segment.protocolVersion,
|
|
6194
|
+
});
|
|
6195
|
+
}
|
|
6196
|
+
|
|
6197
|
+
private enqueueReversionReleaseReference(
|
|
6198
|
+
stored: IStoredSessionState,
|
|
6199
|
+
release: IFlexPendingReversionRelease,
|
|
6200
|
+
): void {
|
|
6201
|
+
if (stored.pendingReversionReleases.some((entry) => entry.captureId === release.captureId)) {
|
|
4736
6202
|
return;
|
|
4737
6203
|
}
|
|
4738
6204
|
if (stored.pendingReversionReleases.length >= this.reversionLimits.maxPendingReversionReleases) {
|
|
4739
6205
|
throw new FlexHarnessValidationError('Pending reversion release limit was reached.');
|
|
4740
6206
|
}
|
|
4741
|
-
stored.pendingReversionReleases.push(
|
|
4742
|
-
runId: segment.runId,
|
|
4743
|
-
captureId: segment.captureId,
|
|
4744
|
-
reference: cloneSerializable(segment.workspaceReference),
|
|
4745
|
-
});
|
|
6207
|
+
stored.pendingReversionReleases.push(cloneSerializable(release));
|
|
4746
6208
|
}
|
|
4747
6209
|
|
|
4748
6210
|
private pruneReversionState(
|
|
@@ -4784,7 +6246,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4784
6246
|
}
|
|
4785
6247
|
if (end <= 0) break;
|
|
4786
6248
|
const prefix = stored.reversionSegments.slice(0, end);
|
|
4787
|
-
if (prefix.some((segment) =>
|
|
6249
|
+
if (prefix.some((segment) =>
|
|
6250
|
+
segment.disposition === 'revertible' && segment.workspaceReference === undefined)) break;
|
|
4788
6251
|
for (const segment of prefix) this.enqueueReversionRelease(stored, segment);
|
|
4789
6252
|
stored.reversionSegments.splice(0, end);
|
|
4790
6253
|
stored.revertCursor = Math.max(
|
|
@@ -4897,6 +6360,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4897
6360
|
}
|
|
4898
6361
|
while (stored.pendingReversionReleases.length > 0) {
|
|
4899
6362
|
const release = stored.pendingReversionReleases[0];
|
|
6363
|
+
if (this.reversionProviderProtocolVersion() !== release.protocolVersion) {
|
|
6364
|
+
throw new FlexHarnessValidationError(
|
|
6365
|
+
`Pending workspace release requires protocolVersion ${release.protocolVersion}.`,
|
|
6366
|
+
);
|
|
6367
|
+
}
|
|
4900
6368
|
try {
|
|
4901
6369
|
await this.withReversionMaintenanceSignal((signal) =>
|
|
4902
6370
|
this.turnReversionProvider!.release(Object.freeze({
|
|
@@ -4947,9 +6415,33 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4947
6415
|
stored?: IStoredSessionState,
|
|
4948
6416
|
): Promise<void> {
|
|
4949
6417
|
if (stored) await stored.projectionQueue;
|
|
4950
|
-
|
|
4951
|
-
if (
|
|
4952
|
-
let projection
|
|
6418
|
+
let durableProjection = await this.stores.projections.load(storageKey, sessionId);
|
|
6419
|
+
if (!durableProjection) return;
|
|
6420
|
+
let projection = this.upgradeProjectionSnapshot(durableProjection);
|
|
6421
|
+
const persistDirect = async (next: IFlexProjectionSnapshotCurrent): Promise<void> => {
|
|
6422
|
+
const before = durableProjection;
|
|
6423
|
+
try {
|
|
6424
|
+
await this.stores.projections.save(storageKey, sessionId, next, projection.revision);
|
|
6425
|
+
durableProjection = next;
|
|
6426
|
+
projection = next;
|
|
6427
|
+
} catch (error) {
|
|
6428
|
+
let current: TFlexProjectionSnapshot | undefined;
|
|
6429
|
+
let reconciliationError: unknown;
|
|
6430
|
+
try {
|
|
6431
|
+
current = await this.stores.projections.load(storageKey, sessionId);
|
|
6432
|
+
} catch (loadError) {
|
|
6433
|
+
reconciliationError = loadError;
|
|
6434
|
+
}
|
|
6435
|
+
if (current && JSON.stringify(current) === JSON.stringify(next)) {
|
|
6436
|
+
durableProjection = current;
|
|
6437
|
+
projection = next;
|
|
6438
|
+
return;
|
|
6439
|
+
}
|
|
6440
|
+
if (current && before && JSON.stringify(current) === JSON.stringify(before)) throw error;
|
|
6441
|
+
state.lifecycle = 'fenced';
|
|
6442
|
+
throw reconciliationError === undefined ? error : combineErrors([error, reconciliationError]);
|
|
6443
|
+
}
|
|
6444
|
+
};
|
|
4953
6445
|
if (projection.pendingReversion?.kind === 'apply') {
|
|
4954
6446
|
if (stored) {
|
|
4955
6447
|
await this.resumePendingApply(state, stored, scopeId, scope);
|
|
@@ -4961,12 +6453,17 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4961
6453
|
for (const runId of orderedRunIds) {
|
|
4962
6454
|
if (pending.appliedRunIds.includes(runId)) continue;
|
|
4963
6455
|
const segment = projection.reversionSegments.find((entry) => entry.runId === runId)!;
|
|
4964
|
-
if (segment.
|
|
6456
|
+
if (segment.disposition === 'revertible') {
|
|
4965
6457
|
if (!this.turnReversionProvider || !segment.captureId || segment.workspaceReference === undefined) {
|
|
4966
6458
|
throw new FlexHarnessValidationError(
|
|
4967
6459
|
'Pending workspace reversion deletion requires its turn reversion provider.',
|
|
4968
6460
|
);
|
|
4969
6461
|
}
|
|
6462
|
+
if (this.reversionProviderProtocolVersion() !== segment.protocolVersion) {
|
|
6463
|
+
throw new FlexHarnessValidationError(
|
|
6464
|
+
`Pending workspace reversion deletion requires protocolVersion ${segment.protocolVersion}.`,
|
|
6465
|
+
);
|
|
6466
|
+
}
|
|
4970
6467
|
const captureId = segment.captureId;
|
|
4971
6468
|
const reference = segment.workspaceReference;
|
|
4972
6469
|
const operationId = `${pending.operationId}:${sha256Hex(runId).slice(0, 16)}`;
|
|
@@ -4985,6 +6482,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4985
6482
|
let inspection = await this.withReversionMaintenanceSignal((signal) =>
|
|
4986
6483
|
this.turnReversionProvider!.inspectApply(createContext(signal)));
|
|
4987
6484
|
if (inspection.status === 'unknown') {
|
|
6485
|
+
state.lifecycle = 'fenced';
|
|
4988
6486
|
throw new FlexHarnessValidationError('Workspace reversion apply outcome is unknown.');
|
|
4989
6487
|
}
|
|
4990
6488
|
if (inspection.status === 'not-applied') {
|
|
@@ -4994,14 +6492,15 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4994
6492
|
} catch (error) {
|
|
4995
6493
|
inspection = await this.withReversionMaintenanceSignal((signal) =>
|
|
4996
6494
|
this.turnReversionProvider!.inspectApply(createContext(signal)));
|
|
4997
|
-
if (inspection.status !== 'applied')
|
|
6495
|
+
if (inspection.status !== 'applied') {
|
|
6496
|
+
if (inspection.status === 'unknown') state.lifecycle = 'fenced';
|
|
6497
|
+
throw error;
|
|
6498
|
+
}
|
|
4998
6499
|
}
|
|
4999
6500
|
}
|
|
5000
6501
|
}
|
|
5001
6502
|
pending.appliedRunIds.push(runId);
|
|
5002
|
-
|
|
5003
|
-
await this.stores.projections.save(storageKey, sessionId, next, projection.revision);
|
|
5004
|
-
projection = next;
|
|
6503
|
+
await persistDirect({ ...projection, revision: projection.revision + 1 });
|
|
5005
6504
|
}
|
|
5006
6505
|
const { pendingReversion: _pendingReversion, ...completedProjection } = projection;
|
|
5007
6506
|
const completed: IFlexProjectionSnapshotCurrent = {
|
|
@@ -5009,23 +6508,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5009
6508
|
revision: projection.revision + 1,
|
|
5010
6509
|
revertCursor: pending.toCursor,
|
|
5011
6510
|
};
|
|
5012
|
-
await
|
|
5013
|
-
projection = completed;
|
|
6511
|
+
await persistDirect(completed);
|
|
5014
6512
|
}
|
|
5015
6513
|
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;
|
|
6514
|
+
if (!recoveredProjection) return;
|
|
6515
|
+
durableProjection = recoveredProjection;
|
|
6516
|
+
projection = this.upgradeProjectionSnapshot(recoveredProjection);
|
|
5029
6517
|
}
|
|
5030
6518
|
const releases = new Map<string, IFlexPendingReversionRelease>();
|
|
5031
6519
|
for (const release of projection.pendingReversionReleases) {
|
|
@@ -5037,12 +6525,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5037
6525
|
runId: segment.runId,
|
|
5038
6526
|
captureId: segment.captureId,
|
|
5039
6527
|
reference: cloneSerializable(segment.workspaceReference),
|
|
6528
|
+
protocolVersion: segment.protocolVersion,
|
|
5040
6529
|
});
|
|
5041
6530
|
}
|
|
5042
6531
|
}
|
|
5043
6532
|
const unresolvedCaptures = new Map<string, {
|
|
5044
6533
|
runId: string;
|
|
5045
6534
|
captureId: string;
|
|
6535
|
+
protocolVersion: 1 | 2;
|
|
5046
6536
|
durableState?: 'preparing' | 'prepared' | 'finalizing';
|
|
5047
6537
|
}>();
|
|
5048
6538
|
for (const segment of projection.reversionSegments) {
|
|
@@ -5050,6 +6540,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5050
6540
|
unresolvedCaptures.set(segment.captureId, {
|
|
5051
6541
|
runId: segment.runId,
|
|
5052
6542
|
captureId: segment.captureId,
|
|
6543
|
+
protocolVersion: segment.protocolVersion,
|
|
5053
6544
|
});
|
|
5054
6545
|
}
|
|
5055
6546
|
}
|
|
@@ -5057,10 +6548,36 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5057
6548
|
unresolvedCaptures.set(projection.pendingReversion.captureId, {
|
|
5058
6549
|
runId: projection.pendingReversion.runId,
|
|
5059
6550
|
captureId: projection.pendingReversion.captureId,
|
|
6551
|
+
protocolVersion: projection.pendingReversion.protocolVersion,
|
|
5060
6552
|
durableState: projection.pendingReversion.state,
|
|
5061
6553
|
});
|
|
5062
6554
|
}
|
|
6555
|
+
if (unresolvedCaptures.size > 0 && !this.turnReversionProvider) {
|
|
6556
|
+
throw new FlexHarnessValidationError(
|
|
6557
|
+
'Capture-backed session deletion requires its turn reversion provider.',
|
|
6558
|
+
);
|
|
6559
|
+
}
|
|
5063
6560
|
for (const capture of unresolvedCaptures.values()) {
|
|
6561
|
+
if (this.reversionProviderProtocolVersion() !== capture.protocolVersion) {
|
|
6562
|
+
throw new FlexHarnessValidationError(
|
|
6563
|
+
`Deleted session capture requires protocolVersion ${capture.protocolVersion}.`,
|
|
6564
|
+
);
|
|
6565
|
+
}
|
|
6566
|
+
if (capture.durableState === 'prepared') {
|
|
6567
|
+
if (stored) {
|
|
6568
|
+
await this.mutateProjection(state, stored, () => {
|
|
6569
|
+
const pending = stored.pendingReversion;
|
|
6570
|
+
if (pending?.kind !== 'capture' || pending.captureId !== capture.captureId) {
|
|
6571
|
+
throw new FlexHarnessValidationError('Deleted session capture changed before finalization.');
|
|
6572
|
+
}
|
|
6573
|
+
pending.state = 'finalizing';
|
|
6574
|
+
}, true);
|
|
6575
|
+
} else if (projection.pendingReversion?.kind === 'capture') {
|
|
6576
|
+
projection.pendingReversion.state = 'finalizing';
|
|
6577
|
+
await persistDirect({ ...projection, revision: projection.revision + 1 });
|
|
6578
|
+
}
|
|
6579
|
+
capture.durableState = 'finalizing';
|
|
6580
|
+
}
|
|
5064
6581
|
let inspection;
|
|
5065
6582
|
try {
|
|
5066
6583
|
inspection = await this.inspectReversionCapture(
|
|
@@ -5081,6 +6598,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5081
6598
|
);
|
|
5082
6599
|
}
|
|
5083
6600
|
if (inspection.status === 'unknown') {
|
|
6601
|
+
state.lifecycle = 'fenced';
|
|
5084
6602
|
throw new FlexHarnessValidationError('Deleted session capture outcome is unknown.');
|
|
5085
6603
|
}
|
|
5086
6604
|
if (inspection.status === 'missing') {
|
|
@@ -5088,10 +6606,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5088
6606
|
state.lifecycle = 'fenced';
|
|
5089
6607
|
throw new FlexHarnessValidationError('Prepared deleted-session capture is missing.');
|
|
5090
6608
|
}
|
|
5091
|
-
let
|
|
6609
|
+
let outcome = inspection.status === 'finalized' ? inspection.outcome : undefined;
|
|
5092
6610
|
if (inspection.status === 'prepared') {
|
|
5093
6611
|
try {
|
|
5094
|
-
|
|
6612
|
+
outcome = await this.finalizeReversionCapture(
|
|
5095
6613
|
scopeId,
|
|
5096
6614
|
scope,
|
|
5097
6615
|
storageKey,
|
|
@@ -5108,7 +6626,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5108
6626
|
capture.runId,
|
|
5109
6627
|
capture.captureId,
|
|
5110
6628
|
);
|
|
5111
|
-
if (finalInspection.status !== 'finalized'
|
|
6629
|
+
if (finalInspection.status !== 'finalized') {
|
|
6630
|
+
if (finalInspection.status === 'unknown') state.lifecycle = 'fenced';
|
|
5112
6631
|
throw this.projectOperationError(
|
|
5113
6632
|
error,
|
|
5114
6633
|
'turnReversion',
|
|
@@ -5117,63 +6636,77 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5117
6636
|
`release:${capture.captureId}`,
|
|
5118
6637
|
);
|
|
5119
6638
|
}
|
|
5120
|
-
|
|
6639
|
+
outcome = finalInspection.outcome;
|
|
5121
6640
|
}
|
|
5122
6641
|
}
|
|
5123
|
-
if (
|
|
6642
|
+
if (outcome === undefined) {
|
|
5124
6643
|
throw new FlexHarnessValidationError('Deleted session capture has no release reference.');
|
|
5125
6644
|
}
|
|
5126
6645
|
releases.set(capture.captureId, {
|
|
5127
6646
|
runId: capture.runId,
|
|
5128
6647
|
captureId: capture.captureId,
|
|
5129
|
-
reference:
|
|
6648
|
+
reference: cloneSerializable(outcome.reference),
|
|
6649
|
+
protocolVersion: capture.protocolVersion,
|
|
5130
6650
|
});
|
|
5131
6651
|
}
|
|
6652
|
+
if (releases.size === 0) return;
|
|
6653
|
+
if (!this.turnReversionProvider) {
|
|
6654
|
+
throw new FlexHarnessValidationError(
|
|
6655
|
+
'Capture-backed session deletion requires its turn reversion provider.',
|
|
6656
|
+
);
|
|
6657
|
+
}
|
|
5132
6658
|
if (stored) {
|
|
5133
6659
|
await this.mutateProjection(state, stored, () => {
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
6660
|
+
stored.reversionSegments = [];
|
|
6661
|
+
stored.revertCursor = 0;
|
|
6662
|
+
delete stored.pendingReversion;
|
|
6663
|
+
stored.pendingReversionReleases = [...releases.values()].map((release) =>
|
|
6664
|
+
cloneSerializable(release));
|
|
5139
6665
|
}, true);
|
|
6666
|
+
await this.drainReversionReleases(state, stored, scopeId, scope, true);
|
|
5140
6667
|
} else {
|
|
5141
|
-
const
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
6668
|
+
const { pendingReversion: _pendingReversion, ...withoutPending } = projection;
|
|
6669
|
+
await persistDirect({
|
|
6670
|
+
...withoutPending,
|
|
6671
|
+
revision: projection.revision + 1,
|
|
6672
|
+
reversionSegments: [],
|
|
6673
|
+
revertCursor: 0,
|
|
6674
|
+
pendingReversionReleases: [...releases.values()].map((release) =>
|
|
6675
|
+
cloneSerializable(release)),
|
|
6676
|
+
});
|
|
6677
|
+
while (projection.pendingReversionReleases.length > 0) {
|
|
6678
|
+
const release = projection.pendingReversionReleases[0];
|
|
6679
|
+
if (this.reversionProviderProtocolVersion() !== release.protocolVersion) {
|
|
6680
|
+
throw new FlexHarnessValidationError(
|
|
6681
|
+
`Deleted session release requires protocolVersion ${release.protocolVersion}.`,
|
|
6682
|
+
);
|
|
6683
|
+
}
|
|
6684
|
+
try {
|
|
6685
|
+
await this.withReversionMaintenanceSignal((signal) =>
|
|
6686
|
+
this.turnReversionProvider!.release(Object.freeze({
|
|
6687
|
+
scopeId,
|
|
6688
|
+
scope,
|
|
6689
|
+
storageKey,
|
|
6690
|
+
sessionId,
|
|
6691
|
+
runId: release.runId,
|
|
6692
|
+
captureId: release.captureId,
|
|
6693
|
+
reference: cloneSerializable(release.reference),
|
|
6694
|
+
signal,
|
|
6695
|
+
})));
|
|
6696
|
+
} catch (error) {
|
|
6697
|
+
throw this.projectOperationError(
|
|
6698
|
+
error,
|
|
6699
|
+
'turnReversion',
|
|
5160
6700
|
scopeId,
|
|
5161
|
-
scope,
|
|
5162
|
-
storageKey,
|
|
5163
6701
|
sessionId,
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
'turnReversion',
|
|
5173
|
-
scopeId,
|
|
5174
|
-
sessionId,
|
|
5175
|
-
`release:${release.captureId}`,
|
|
5176
|
-
);
|
|
6702
|
+
`release:${release.captureId}`,
|
|
6703
|
+
);
|
|
6704
|
+
}
|
|
6705
|
+
await persistDirect({
|
|
6706
|
+
...projection,
|
|
6707
|
+
revision: projection.revision + 1,
|
|
6708
|
+
pendingReversionReleases: projection.pendingReversionReleases.slice(1),
|
|
6709
|
+
});
|
|
5177
6710
|
}
|
|
5178
6711
|
}
|
|
5179
6712
|
}
|
|
@@ -5831,6 +7364,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5831
7364
|
throw new FlexHarnessValidationError('Tool handle close must be a function.');
|
|
5832
7365
|
}
|
|
5833
7366
|
const tools = handle.tools;
|
|
7367
|
+
this.assertNoBuiltInToolCollisions(run, tools);
|
|
5834
7368
|
const closeHandle = close;
|
|
5835
7369
|
return {
|
|
5836
7370
|
tools,
|
|
@@ -6170,6 +7704,28 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6170
7704
|
sessions: [],
|
|
6171
7705
|
tombstones: [],
|
|
6172
7706
|
} satisfies IFlexScopeSnapshot);
|
|
7707
|
+
let scopeChanged = false;
|
|
7708
|
+
const repairedGenerationSequence = snapshot.revision + 1;
|
|
7709
|
+
for (const session of snapshot.sessions) {
|
|
7710
|
+
if (session.sessionGenerationId !== undefined) continue;
|
|
7711
|
+
session.sessionGenerationId = deriveLegacySessionGenerationId(
|
|
7712
|
+
storageKey,
|
|
7713
|
+
session.sessionId,
|
|
7714
|
+
session.createdAt,
|
|
7715
|
+
);
|
|
7716
|
+
session.sessionGenerationSequence = repairedGenerationSequence;
|
|
7717
|
+
scopeChanged = true;
|
|
7718
|
+
}
|
|
7719
|
+
for (const tombstone of snapshot.tombstones) {
|
|
7720
|
+
if (tombstone.sessionGenerationId !== undefined) continue;
|
|
7721
|
+
tombstone.sessionGenerationId = deriveLegacySessionGenerationId(
|
|
7722
|
+
storageKey,
|
|
7723
|
+
tombstone.sessionId,
|
|
7724
|
+
tombstone.deletedAt,
|
|
7725
|
+
);
|
|
7726
|
+
tombstone.sessionGenerationSequence = repairedGenerationSequence;
|
|
7727
|
+
scopeChanged = true;
|
|
7728
|
+
}
|
|
6173
7729
|
const state: IStorageState = {
|
|
6174
7730
|
storageKey,
|
|
6175
7731
|
compactorLifecycleController,
|
|
@@ -6192,7 +7748,6 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6192
7748
|
detachedCleanups: new Set(),
|
|
6193
7749
|
};
|
|
6194
7750
|
const loadedSessions: IStoredSessionState[] = [];
|
|
6195
|
-
let scopeChanged = false;
|
|
6196
7751
|
try {
|
|
6197
7752
|
for (const metadata of snapshot.sessions) {
|
|
6198
7753
|
if (state.tombstones.has(metadata.sessionId)) continue;
|
|
@@ -6244,12 +7799,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6244
7799
|
scopeId,
|
|
6245
7800
|
scope,
|
|
6246
7801
|
);
|
|
6247
|
-
await this.cleanupSessionDomains(storageKey, tombstone
|
|
7802
|
+
await this.cleanupSessionDomains(storageKey, tombstone);
|
|
6248
7803
|
}
|
|
6249
7804
|
for (const tombstone of group) state.tombstones.delete(tombstone.sessionId);
|
|
6250
7805
|
scopeChanged = true;
|
|
6251
|
-
} catch {
|
|
7806
|
+
} catch (error) {
|
|
6252
7807
|
// A retained tombstone is retried by the next load or explicit delete call.
|
|
7808
|
+
if (state.lifecycle === 'fenced') {
|
|
7809
|
+
this.deferNamespaceDrain(state, error, { scopeId, scope });
|
|
7810
|
+
break;
|
|
7811
|
+
}
|
|
6253
7812
|
}
|
|
6254
7813
|
}
|
|
6255
7814
|
if (scopeChanged) {
|
|
@@ -6305,7 +7864,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6305
7864
|
revertCursor: 0,
|
|
6306
7865
|
excludedRunIds: [],
|
|
6307
7866
|
pendingReversionReleases: [],
|
|
6308
|
-
projectionSchemaVersion:
|
|
7867
|
+
projectionSchemaVersion: 3,
|
|
6309
7868
|
projectionReconciliationRequired: false,
|
|
6310
7869
|
projectionRevision: 0,
|
|
6311
7870
|
projectionQueue: Promise.resolve(),
|
|
@@ -6373,7 +7932,28 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6373
7932
|
throw combineErrors(acquisitionErrors);
|
|
6374
7933
|
}
|
|
6375
7934
|
const projection = projectionResult.value;
|
|
6376
|
-
const
|
|
7935
|
+
const legacyProjection = projection?.schemaVersion === 2 ? projection : undefined;
|
|
7936
|
+
const currentProjection = projection?.schemaVersion === 3 ? projection : undefined;
|
|
7937
|
+
const reversionSegments: IFlexReversionSegment[] = currentProjection
|
|
7938
|
+
? cloneSerializable(currentProjection.reversionSegments)
|
|
7939
|
+
: (legacyProjection?.reversionSegments ?? []).map((segment): IFlexReversionSegment => ({
|
|
7940
|
+
...cloneSerializable(segment),
|
|
7941
|
+
protocolVersion: 1,
|
|
7942
|
+
provenance: segment.workspaceCaptured ? 'workspace' : 'transcript',
|
|
7943
|
+
...(segment.workspaceCaptured
|
|
7944
|
+
? { disposition: segment.status === 'capturing' ? 'pending' : 'revertible' }
|
|
7945
|
+
: {}),
|
|
7946
|
+
}));
|
|
7947
|
+
const pendingReversion = currentProjection?.pendingReversion
|
|
7948
|
+
?? (legacyProjection?.pendingReversion?.kind === 'capture'
|
|
7949
|
+
? { ...legacyProjection.pendingReversion, protocolVersion: 1 as const }
|
|
7950
|
+
: legacyProjection?.pendingReversion);
|
|
7951
|
+
const pendingReversionReleases: IFlexPendingReversionRelease[] = currentProjection
|
|
7952
|
+
? cloneSerializable(currentProjection.pendingReversionReleases)
|
|
7953
|
+
: (legacyProjection?.pendingReversionReleases ?? []).map((release) => ({
|
|
7954
|
+
...cloneSerializable(release),
|
|
7955
|
+
protocolVersion: 1,
|
|
7956
|
+
}));
|
|
6377
7957
|
const permission = permissionResult.value;
|
|
6378
7958
|
const eventStore = eventStoreResult.value;
|
|
6379
7959
|
const jobStore = jobStoreResult.value;
|
|
@@ -6404,18 +7984,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6404
7984
|
session: cloneSerializable(metadata),
|
|
6405
7985
|
messages: cloneSerializable(projection?.messages ?? []),
|
|
6406
7986
|
stagedTerminals: cloneSerializable(projection?.stagedTerminals ?? []),
|
|
6407
|
-
reversionSegments
|
|
6408
|
-
revertCursor:
|
|
6409
|
-
excludedRunIds: cloneSerializable(
|
|
6410
|
-
|
|
6411
|
-
? {}
|
|
6412
|
-
: { pendingReversion: cloneSerializable(projectionV2.pendingReversion) }),
|
|
6413
|
-
pendingReversionReleases: cloneSerializable(
|
|
6414
|
-
projectionV2?.pendingReversionReleases ?? [],
|
|
7987
|
+
reversionSegments,
|
|
7988
|
+
revertCursor: currentProjection?.revertCursor ?? legacyProjection?.revertCursor ?? 0,
|
|
7989
|
+
excludedRunIds: cloneSerializable(
|
|
7990
|
+
currentProjection?.excludedRunIds ?? legacyProjection?.excludedRunIds ?? [],
|
|
6415
7991
|
),
|
|
6416
|
-
|
|
7992
|
+
...(pendingReversion === undefined
|
|
7993
|
+
? {}
|
|
7994
|
+
: { pendingReversion: cloneSerializable(pendingReversion) }),
|
|
7995
|
+
pendingReversionReleases,
|
|
7996
|
+
projectionSchemaVersion: projection?.schemaVersion ?? 3,
|
|
6417
7997
|
projectionReconciliationRequired: false,
|
|
6418
|
-
...(projection
|
|
7998
|
+
...(projection && projection.schemaVersion !== 3
|
|
6419
7999
|
? { projectionBaseline: cloneSerializable(projection) }
|
|
6420
8000
|
: {}),
|
|
6421
8001
|
projectionRevision: projection?.revision ?? 0,
|
|
@@ -6585,6 +8165,31 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6585
8165
|
'Capture-backed session recovery requires its turn reversion provider.',
|
|
6586
8166
|
);
|
|
6587
8167
|
}
|
|
8168
|
+
const requiredProtocols = new Set<number>([
|
|
8169
|
+
...stored.reversionSegments
|
|
8170
|
+
.filter((segment) => segment.captureId !== undefined)
|
|
8171
|
+
.map((segment) => segment.protocolVersion),
|
|
8172
|
+
...stored.pendingReversionReleases.map((release) => release.protocolVersion),
|
|
8173
|
+
...(stored.pendingReversion?.kind === 'capture'
|
|
8174
|
+
? [stored.pendingReversion.protocolVersion]
|
|
8175
|
+
: []),
|
|
8176
|
+
]);
|
|
8177
|
+
const providerProtocol = this.reversionProviderProtocolVersion();
|
|
8178
|
+
if (requiredProtocols.size > 0 && (
|
|
8179
|
+
requiredProtocols.size !== 1
|
|
8180
|
+
|| !requiredProtocols.has(providerProtocol ?? 0)
|
|
8181
|
+
)) {
|
|
8182
|
+
throw new FlexHarnessValidationError(
|
|
8183
|
+
`Capture-backed session recovery requires protocolVersion ${[...requiredProtocols].join(', ')}.`,
|
|
8184
|
+
);
|
|
8185
|
+
}
|
|
8186
|
+
if (stored.pendingReversion?.kind === 'capture' && stored.pendingReversionReleases.length > 0) {
|
|
8187
|
+
if (!state || scopeId === undefined || scope === undefined) {
|
|
8188
|
+
throw new FlexHarnessValidationError('Pending capture recovery is missing its scope context.');
|
|
8189
|
+
}
|
|
8190
|
+
await this.drainReversionReleases(state, stored, scopeId, scope, false);
|
|
8191
|
+
this.assertStateAcceptingWork(state);
|
|
8192
|
+
}
|
|
6588
8193
|
const outcomes = this.canonicalOutcomes(stored.agentSession.getEvents());
|
|
6589
8194
|
const firstHiddenCandidate = this.completedReversionCandidates(stored)[stored.revertCursor];
|
|
6590
8195
|
const reversionVisibilityBoundary = firstHiddenCandidate
|
|
@@ -6698,7 +8303,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6698
8303
|
snapshot,
|
|
6699
8304
|
expected,
|
|
6700
8305
|
);
|
|
6701
|
-
stored.projectionSchemaVersion =
|
|
8306
|
+
stored.projectionSchemaVersion = 3;
|
|
6702
8307
|
delete stored.projectionBaseline;
|
|
6703
8308
|
stored.projectionReconciliationRequired = false;
|
|
6704
8309
|
stored.projectionRevision = snapshot.revision;
|
|
@@ -6875,7 +8480,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6875
8480
|
: cloneSerializable(stored.pendingReversion);
|
|
6876
8481
|
const beforePendingReversionReleases = cloneSerializable(stored.pendingReversionReleases);
|
|
6877
8482
|
const beforeSnapshot: TFlexProjectionSnapshot = beforeBaseline ?? {
|
|
6878
|
-
schemaVersion:
|
|
8483
|
+
schemaVersion: 3,
|
|
6879
8484
|
revision: beforeRevision,
|
|
6880
8485
|
messages: cloneSerializable(beforeMessages),
|
|
6881
8486
|
stagedTerminals: cloneSerializable(beforeStages),
|
|
@@ -6917,7 +8522,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6917
8522
|
snapshot,
|
|
6918
8523
|
beforeRevision,
|
|
6919
8524
|
);
|
|
6920
|
-
stored.projectionSchemaVersion =
|
|
8525
|
+
stored.projectionSchemaVersion = 3;
|
|
6921
8526
|
delete stored.projectionBaseline;
|
|
6922
8527
|
stored.projectionReconciliationRequired = false;
|
|
6923
8528
|
stored.projectionRevision = snapshot.revision;
|
|
@@ -6928,7 +8533,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6928
8533
|
throw error;
|
|
6929
8534
|
}
|
|
6930
8535
|
if (error instanceof FlexHarnessStoreCommitUncertainError) {
|
|
6931
|
-
stored.projectionSchemaVersion =
|
|
8536
|
+
stored.projectionSchemaVersion = 3;
|
|
6932
8537
|
stored.projectionReconciliationRequired = true;
|
|
6933
8538
|
stored.projectionRevision = snapshot.revision;
|
|
6934
8539
|
state.lifecycle = 'fenced';
|
|
@@ -6945,7 +8550,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6945
8550
|
reconciliationError = loadError;
|
|
6946
8551
|
}
|
|
6947
8552
|
if (current && JSON.stringify(current) === JSON.stringify(snapshot)) {
|
|
6948
|
-
stored.projectionSchemaVersion =
|
|
8553
|
+
stored.projectionSchemaVersion = 3;
|
|
6949
8554
|
delete stored.projectionBaseline;
|
|
6950
8555
|
stored.projectionReconciliationRequired = false;
|
|
6951
8556
|
stored.projectionRevision = snapshot.revision;
|
|
@@ -6958,7 +8563,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6958
8563
|
restore();
|
|
6959
8564
|
throw error;
|
|
6960
8565
|
}
|
|
6961
|
-
stored.projectionSchemaVersion =
|
|
8566
|
+
stored.projectionSchemaVersion = 3;
|
|
6962
8567
|
stored.projectionReconciliationRequired = true;
|
|
6963
8568
|
stored.projectionRevision = snapshot.revision;
|
|
6964
8569
|
state.lifecycle = 'fenced';
|
|
@@ -7070,7 +8675,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7070
8675
|
revision: number,
|
|
7071
8676
|
): IFlexProjectionSnapshotCurrent {
|
|
7072
8677
|
return {
|
|
7073
|
-
schemaVersion:
|
|
8678
|
+
schemaVersion: 3,
|
|
7074
8679
|
revision,
|
|
7075
8680
|
messages: cloneSerializable(stored.messages),
|
|
7076
8681
|
stagedTerminals: cloneSerializable(stored.stagedTerminals),
|
|
@@ -7084,6 +8689,51 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7084
8689
|
};
|
|
7085
8690
|
}
|
|
7086
8691
|
|
|
8692
|
+
private upgradeProjectionSnapshot(
|
|
8693
|
+
projection: TFlexProjectionSnapshot,
|
|
8694
|
+
): IFlexProjectionSnapshotCurrent {
|
|
8695
|
+
if (projection.schemaVersion === 3) return cloneSerializable(projection);
|
|
8696
|
+
if (projection.schemaVersion === 1) {
|
|
8697
|
+
return {
|
|
8698
|
+
schemaVersion: 3,
|
|
8699
|
+
revision: projection.revision,
|
|
8700
|
+
messages: cloneSerializable(projection.messages),
|
|
8701
|
+
stagedTerminals: cloneSerializable(projection.stagedTerminals),
|
|
8702
|
+
reversionSegments: [],
|
|
8703
|
+
revertCursor: 0,
|
|
8704
|
+
excludedRunIds: [],
|
|
8705
|
+
pendingReversionReleases: [],
|
|
8706
|
+
};
|
|
8707
|
+
}
|
|
8708
|
+
return {
|
|
8709
|
+
schemaVersion: 3,
|
|
8710
|
+
revision: projection.revision,
|
|
8711
|
+
messages: cloneSerializable(projection.messages),
|
|
8712
|
+
stagedTerminals: cloneSerializable(projection.stagedTerminals),
|
|
8713
|
+
reversionSegments: projection.reversionSegments.map((segment) => ({
|
|
8714
|
+
...cloneSerializable(segment),
|
|
8715
|
+
protocolVersion: 1,
|
|
8716
|
+
provenance: segment.workspaceCaptured ? 'workspace' as const : 'transcript' as const,
|
|
8717
|
+
...(segment.workspaceCaptured
|
|
8718
|
+
? { disposition: segment.status === 'capturing' ? 'pending' as const : 'revertible' as const }
|
|
8719
|
+
: {}),
|
|
8720
|
+
})),
|
|
8721
|
+
revertCursor: projection.revertCursor,
|
|
8722
|
+
excludedRunIds: cloneSerializable(projection.excludedRunIds),
|
|
8723
|
+
...(projection.pendingReversion === undefined
|
|
8724
|
+
? {}
|
|
8725
|
+
: {
|
|
8726
|
+
pendingReversion: projection.pendingReversion.kind === 'capture'
|
|
8727
|
+
? { ...cloneSerializable(projection.pendingReversion), protocolVersion: 1 }
|
|
8728
|
+
: cloneSerializable(projection.pendingReversion),
|
|
8729
|
+
}),
|
|
8730
|
+
pendingReversionReleases: projection.pendingReversionReleases.map((release) => ({
|
|
8731
|
+
...cloneSerializable(release),
|
|
8732
|
+
protocolVersion: 1,
|
|
8733
|
+
})),
|
|
8734
|
+
};
|
|
8735
|
+
}
|
|
8736
|
+
|
|
7087
8737
|
private async reconcileProjectionFromStore(stored: IStoredSessionState): Promise<void> {
|
|
7088
8738
|
if (!stored.projectionReconciliationRequired) return;
|
|
7089
8739
|
const projection = await this.stores.projections.load(
|
|
@@ -7093,35 +8743,42 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7093
8743
|
if (!projection) {
|
|
7094
8744
|
throw new FlexHarnessValidationError('Uncertain projection persistence could not be reloaded.');
|
|
7095
8745
|
}
|
|
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
|
-
}
|
|
8746
|
+
const current = this.upgradeProjectionSnapshot(projection);
|
|
8747
|
+
stored.messages = current.messages;
|
|
8748
|
+
stored.stagedTerminals = current.stagedTerminals;
|
|
8749
|
+
stored.reversionSegments = current.reversionSegments;
|
|
8750
|
+
stored.revertCursor = current.revertCursor;
|
|
8751
|
+
stored.excludedRunIds = current.excludedRunIds;
|
|
8752
|
+
if (current.pendingReversion === undefined) delete stored.pendingReversion;
|
|
8753
|
+
else stored.pendingReversion = current.pendingReversion;
|
|
8754
|
+
stored.pendingReversionReleases = current.pendingReversionReleases;
|
|
8755
|
+
if (projection.schemaVersion === 3) delete stored.projectionBaseline;
|
|
8756
|
+
else stored.projectionBaseline = cloneSerializable(projection);
|
|
7114
8757
|
stored.projectionSchemaVersion = projection.schemaVersion;
|
|
7115
8758
|
stored.projectionRevision = projection.revision;
|
|
7116
8759
|
stored.projectionReconciliationRequired = false;
|
|
7117
8760
|
}
|
|
7118
8761
|
|
|
7119
|
-
private async cleanupSessionDomains(
|
|
8762
|
+
private async cleanupSessionDomains(
|
|
8763
|
+
storageKey: string,
|
|
8764
|
+
tombstone: IFlexSessionTombstone,
|
|
8765
|
+
): Promise<void> {
|
|
8766
|
+
const sessionId = tombstone.sessionId;
|
|
8767
|
+
const projectManagementCleanup = async (): Promise<void> => {
|
|
8768
|
+
await this.projectManagementQueues.get(
|
|
8769
|
+
this.projectManagementKey(storageKey, sessionId),
|
|
8770
|
+
);
|
|
8771
|
+
await this.tombstoneProjectManagementSession(
|
|
8772
|
+
storageKey,
|
|
8773
|
+
tombstone,
|
|
8774
|
+
);
|
|
8775
|
+
};
|
|
7120
8776
|
const results = await Promise.allSettled([
|
|
7121
8777
|
this.stores.agentEvents.deleteSession(storageKey, sessionId),
|
|
7122
8778
|
this.stores.jobs.deleteSession(storageKey, sessionId),
|
|
7123
8779
|
this.stores.projections.deleteSession(storageKey, sessionId),
|
|
7124
8780
|
this.stores.permissions.deleteSession(storageKey, sessionId),
|
|
8781
|
+
projectManagementCleanup(),
|
|
7125
8782
|
]);
|
|
7126
8783
|
const errors = results
|
|
7127
8784
|
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
@@ -7129,6 +8786,66 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7129
8786
|
if (errors.length > 0) throw combineErrors(errors);
|
|
7130
8787
|
}
|
|
7131
8788
|
|
|
8789
|
+
private async tombstoneProjectManagementSession(
|
|
8790
|
+
storageKey: string,
|
|
8791
|
+
sessionTombstone: IFlexSessionTombstone,
|
|
8792
|
+
): Promise<void> {
|
|
8793
|
+
const store = this.stores.projectManagement;
|
|
8794
|
+
const sessionId = sessionTombstone.sessionId;
|
|
8795
|
+
const generation = requireSessionGeneration(sessionTombstone);
|
|
8796
|
+
let lastConflict: FlexHarnessStoreConflictError | undefined;
|
|
8797
|
+
for (let attempt = 0; attempt <= maxProjectManagementTombstoneConflicts; attempt++) {
|
|
8798
|
+
const current = await store.load(storageKey, sessionId);
|
|
8799
|
+
if (current !== undefined) {
|
|
8800
|
+
assertFlexProjectManagementRecord(current);
|
|
8801
|
+
if (current.sessionGenerationId === generation.sessionGenerationId) {
|
|
8802
|
+
if (current.sessionGenerationSequence !== generation.sessionGenerationSequence) {
|
|
8803
|
+
throw new FlexHarnessStoreConflictError(
|
|
8804
|
+
this.projectManagementKey(storageKey, sessionId),
|
|
8805
|
+
0,
|
|
8806
|
+
current.revision,
|
|
8807
|
+
);
|
|
8808
|
+
}
|
|
8809
|
+
if ('deletedAt' in current) return;
|
|
8810
|
+
} else if (
|
|
8811
|
+
!('deletedAt' in current)
|
|
8812
|
+
|| current.sessionGenerationSequence >= generation.sessionGenerationSequence
|
|
8813
|
+
) {
|
|
8814
|
+
throw new FlexHarnessStoreConflictError(
|
|
8815
|
+
this.projectManagementKey(storageKey, sessionId),
|
|
8816
|
+
0,
|
|
8817
|
+
current.revision,
|
|
8818
|
+
);
|
|
8819
|
+
}
|
|
8820
|
+
}
|
|
8821
|
+
if (attempt === maxProjectManagementTombstoneConflicts) break;
|
|
8822
|
+
const expectedRevision = current === undefined
|
|
8823
|
+
|| current.sessionGenerationId !== generation.sessionGenerationId
|
|
8824
|
+
? 0
|
|
8825
|
+
: current.revision;
|
|
8826
|
+
const tombstone: IFlexProjectManagementTombstone = {
|
|
8827
|
+
schemaVersion: 1,
|
|
8828
|
+
revision: expectedRevision + 1,
|
|
8829
|
+
...generation,
|
|
8830
|
+
deletedAt: sessionTombstone.deletedAt,
|
|
8831
|
+
};
|
|
8832
|
+
try {
|
|
8833
|
+
await store.tombstoneSession(
|
|
8834
|
+
storageKey,
|
|
8835
|
+
sessionId,
|
|
8836
|
+
tombstone,
|
|
8837
|
+
expectedRevision,
|
|
8838
|
+
);
|
|
8839
|
+
} catch (error) {
|
|
8840
|
+
if (!(error instanceof FlexHarnessStoreConflictError)) throw error;
|
|
8841
|
+
lastConflict = error;
|
|
8842
|
+
}
|
|
8843
|
+
}
|
|
8844
|
+
throw lastConflict ?? new FlexHarnessValidationError(
|
|
8845
|
+
`Project management tombstone for session "${sessionId}" could not be durably confirmed.`,
|
|
8846
|
+
);
|
|
8847
|
+
}
|
|
8848
|
+
|
|
7132
8849
|
private collectSessionSubtree(
|
|
7133
8850
|
state: IStorageState,
|
|
7134
8851
|
rootSessionId: string,
|
|
@@ -7324,7 +9041,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7324
9041
|
releaseContext.scope,
|
|
7325
9042
|
retained?.stored,
|
|
7326
9043
|
);
|
|
7327
|
-
await this.cleanupSessionDomains(state.storageKey,
|
|
9044
|
+
await this.cleanupSessionDomains(state.storageKey, tombstone);
|
|
7328
9045
|
if (retained) retained.domainsCompleted = true;
|
|
7329
9046
|
}
|
|
7330
9047
|
}
|
|
@@ -7404,6 +9121,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7404
9121
|
await Promise.allSettled([...state.sessionInitializations.values()]);
|
|
7405
9122
|
cleanupErrors.push(...await this.closeOrphanedResources(state.storageKey));
|
|
7406
9123
|
await state.scopeQueue;
|
|
9124
|
+
await this.awaitProjectManagementOperations(
|
|
9125
|
+
state.storageKey,
|
|
9126
|
+
[
|
|
9127
|
+
...state.sessions.keys(),
|
|
9128
|
+
...state.retainedSessionCleanups.keys(),
|
|
9129
|
+
],
|
|
9130
|
+
);
|
|
7407
9131
|
const currentTombstoneRoot = state.tombstones.get(currentRun.sessionId)?.rootSessionId
|
|
7408
9132
|
?? currentRun.sessionId;
|
|
7409
9133
|
const currentTombstoneCleanup = state.tombstoneCleanups.get(currentTombstoneRoot);
|
|
@@ -7807,12 +9531,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7807
9531
|
const scope = await this.scopeResolver.resolveScope(scopeId);
|
|
7808
9532
|
this.assertOpen();
|
|
7809
9533
|
validateIdentifier(scope.storageKey, 'resolved storageKey');
|
|
7810
|
-
|
|
7811
|
-
if (invocationOwner?.storageKey === scope.storageKey) {
|
|
7812
|
-
throw this.trustInternalError(
|
|
7813
|
-
new FlexHarnessSlashCommandReentryError(invocationOwner.sessionId),
|
|
7814
|
-
);
|
|
7815
|
-
}
|
|
9534
|
+
this.assertNoAmbientSlashCommandTeardown({ storageKey: scope.storageKey });
|
|
7816
9535
|
const errors: unknown[] = [];
|
|
7817
9536
|
const listingSettlement = this.abortSlashCommandListings(
|
|
7818
9537
|
scope.storageKey,
|
|
@@ -7895,8 +9614,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7895
9614
|
return;
|
|
7896
9615
|
}
|
|
7897
9616
|
if (state.lifecycle !== 'fenced') state.lifecycle = 'retiring';
|
|
7898
|
-
const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
|
|
7899
9617
|
const errors: unknown[] = [];
|
|
9618
|
+
const listingSettlement = this.abortSlashCommandListings(storageKey, reason);
|
|
9619
|
+
if (listingSettlement) await listingSettlement;
|
|
9620
|
+
await this.abortSlashCommandExecutions(storageKey, reason, errors);
|
|
9621
|
+
const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
|
|
7900
9622
|
const contextFor = (sessionId: string) => invocation
|
|
7901
9623
|
? this.createCompactorContext(invocation.scopeId, invocation.scope, storageKey, sessionId)
|
|
7902
9624
|
: undefined;
|
|
@@ -7939,6 +9661,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7939
9661
|
await Promise.allSettled([...state.sessionInitializations.values()]);
|
|
7940
9662
|
errors.push(...await this.closeOrphanedResources(state.storageKey));
|
|
7941
9663
|
await state.scopeQueue;
|
|
9664
|
+
await this.awaitProjectManagementOperations(
|
|
9665
|
+
state.storageKey,
|
|
9666
|
+
[
|
|
9667
|
+
...state.sessions.keys(),
|
|
9668
|
+
...state.retainedSessionCleanups.keys(),
|
|
9669
|
+
],
|
|
9670
|
+
);
|
|
7942
9671
|
await Promise.all([...state.sessions.values()].flatMap((stored) => [
|
|
7943
9672
|
stored.projectionQueue,
|
|
7944
9673
|
stored.permissionQueue,
|
|
@@ -8061,6 +9790,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8061
9790
|
if (errors.length > 0) throw combineErrors(errors);
|
|
8062
9791
|
this.compactorInvocationContext.disable();
|
|
8063
9792
|
this.slashCommandInvocationContext.disable();
|
|
9793
|
+
this.slashCommandActivityContext.disable();
|
|
8064
9794
|
this.stateLoads.clear();
|
|
8065
9795
|
this.scopeAdmissions.clear();
|
|
8066
9796
|
this.scopeRetirements.clear();
|
|
@@ -8171,6 +9901,23 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8171
9901
|
}
|
|
8172
9902
|
const state = await stateLoad;
|
|
8173
9903
|
this.assertOpen();
|
|
9904
|
+
if (state.lifecycle === 'fenced') {
|
|
9905
|
+
const drain = this.storageDrains.get(scope.storageKey) ?? this.drainStorage(
|
|
9906
|
+
scope.storageKey,
|
|
9907
|
+
stateLoad,
|
|
9908
|
+
this.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.')),
|
|
9909
|
+
{ scopeId, scope: scope.scope },
|
|
9910
|
+
);
|
|
9911
|
+
if (this.ambientSlashCommandOwner({ storageKey: scope.storageKey })) {
|
|
9912
|
+
throw this.createScopeRetirementError();
|
|
9913
|
+
}
|
|
9914
|
+
try {
|
|
9915
|
+
await drain;
|
|
9916
|
+
} catch {
|
|
9917
|
+
// Fenced cleanup ownership remains available to explicit retirement or disposal.
|
|
9918
|
+
}
|
|
9919
|
+
throw this.createScopeRetirementError();
|
|
9920
|
+
}
|
|
8174
9921
|
if (
|
|
8175
9922
|
admission.retiring
|
|
8176
9923
|
|| admission.generation !== generation
|
|
@@ -8205,6 +9952,29 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8205
9952
|
return this.trustInternalError(new FlexHarnessAbortError(scopeRetirementMessage));
|
|
8206
9953
|
}
|
|
8207
9954
|
|
|
9955
|
+
private ambientSlashCommandOwner(
|
|
9956
|
+
criteria: { scopeId?: string; storageKey?: string; sessionId?: string } = {},
|
|
9957
|
+
): ISlashCommandInvocationOwner | ISlashCommandActivityOwner | undefined {
|
|
9958
|
+
const matches = (owner: ISlashCommandInvocationOwner | ISlashCommandActivityOwner) => (
|
|
9959
|
+
(criteria.scopeId === undefined || owner.scopeId === criteria.scopeId)
|
|
9960
|
+
&& (criteria.storageKey === undefined || owner.storageKey === criteria.storageKey)
|
|
9961
|
+
&& (criteria.sessionId === undefined || owner.sessionId === criteria.sessionId)
|
|
9962
|
+
);
|
|
9963
|
+
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
9964
|
+
if (invocationOwner && matches(invocationOwner)) return invocationOwner;
|
|
9965
|
+
const activityOwner = this.slashCommandActivityContext.getStore();
|
|
9966
|
+
return activityOwner && matches(activityOwner) ? activityOwner : undefined;
|
|
9967
|
+
}
|
|
9968
|
+
|
|
9969
|
+
private assertNoAmbientSlashCommandTeardown(
|
|
9970
|
+
criteria: { scopeId?: string; storageKey?: string; sessionId?: string } = {},
|
|
9971
|
+
): void {
|
|
9972
|
+
const owner = this.ambientSlashCommandOwner(criteria);
|
|
9973
|
+
if (owner) {
|
|
9974
|
+
throw this.trustInternalError(new FlexHarnessSlashCommandReentryError(owner.sessionId));
|
|
9975
|
+
}
|
|
9976
|
+
}
|
|
9977
|
+
|
|
8208
9978
|
private assertStateAcceptingWork(state: IStorageState): void {
|
|
8209
9979
|
if (state.lifecycle !== 'active' || this.storageDrains.has(state.storageKey)) {
|
|
8210
9980
|
throw this.createScopeRetirementError();
|
|
@@ -8259,8 +10029,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8259
10029
|
}
|
|
8260
10030
|
|
|
8261
10031
|
private visibleMessages(stored: IStoredSessionState): IFlexMessage[] {
|
|
8262
|
-
const
|
|
8263
|
-
const firstHiddenSegment = firstHiddenUnit?.segments[0];
|
|
10032
|
+
const firstHiddenSegment = this.reversionGroups(stored)[stored.revertCursor]?.segments[0];
|
|
8264
10033
|
if (!firstHiddenSegment) return stored.messages;
|
|
8265
10034
|
const boundary = stored.messages.findIndex((message) => message.runId === firstHiddenSegment.runId);
|
|
8266
10035
|
return boundary < 0 ? stored.messages : stored.messages.slice(0, boundary);
|