@modelprofile.com/flexharness 3.7.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +9 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +14 -2
- package/dist_ts/classes.flexharness.js +667 -199
- package/dist_ts/interfaces.d.ts +90 -6
- package/dist_ts/interfaces.js +5 -1
- package/dist_ts/utils.json.d.ts +3 -2
- package/dist_ts/utils.json.js +243 -11
- package/dist_ts_migration/v3_legacyflexharness.js +1 -1
- package/package.json +1 -1
- package/readme.hints.md +3 -2
- package/readme.md +86 -10
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +757 -198
- package/ts/classes.stores.ts +3 -3
- package/ts/interfaces.ts +125 -6
- package/ts/utils.json.ts +271 -12
- package/ts_migration/v3_legacyflexharness.ts +1 -1
package/ts/classes.stores.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
IFlexHarnessStores,
|
|
11
11
|
IFlexPermissionSnapshot,
|
|
12
12
|
IFlexPermissionStore,
|
|
13
|
-
|
|
13
|
+
IFlexProjectionSnapshotV3,
|
|
14
14
|
IFlexProjectionStore,
|
|
15
15
|
IFlexScopeSnapshot,
|
|
16
16
|
IFlexScopeStore,
|
|
@@ -365,7 +365,7 @@ class InMemoryProjectionStore implements IFlexProjectionStore {
|
|
|
365
365
|
public async save(
|
|
366
366
|
storageKey: string,
|
|
367
367
|
sessionId: string,
|
|
368
|
-
snapshot:
|
|
368
|
+
snapshot: IFlexProjectionSnapshotV3,
|
|
369
369
|
expectedRevision: number,
|
|
370
370
|
): Promise<void> {
|
|
371
371
|
const key = providerKey(storageKey, sessionId);
|
|
@@ -988,7 +988,7 @@ class JsonProjectionStore implements IFlexProjectionStore {
|
|
|
988
988
|
public async save(
|
|
989
989
|
storageKey: string,
|
|
990
990
|
sessionId: string,
|
|
991
|
-
snapshot:
|
|
991
|
+
snapshot: IFlexProjectionSnapshotV3,
|
|
992
992
|
expectedRevision: number,
|
|
993
993
|
): Promise<void> {
|
|
994
994
|
const validated = validateSnapshotSave(snapshot, expectedRevision, assertFlexProjectionSnapshot);
|
package/ts/interfaces.ts
CHANGED
|
@@ -460,8 +460,16 @@ export interface IFlexProjectionSnapshotV1 {
|
|
|
460
460
|
}
|
|
461
461
|
|
|
462
462
|
export type TFlexReversionSegmentStatus = 'capturing' | 'completed' | 'failed' | 'cancelled';
|
|
463
|
+
export type TFlexReversionProtocolVersion = 1 | 2;
|
|
464
|
+
export type TFlexReversionProvenance = 'transcript' | 'workspace';
|
|
465
|
+
export type TFlexReversionDisposition = 'pending' | 'revertible' | 'no-change' | 'nonrevertible';
|
|
463
466
|
|
|
464
|
-
export interface
|
|
467
|
+
export interface IFlexAffectedWorkspace {
|
|
468
|
+
readonly id: string;
|
|
469
|
+
readonly label: string;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
interface IFlexReversionSegmentV2 {
|
|
465
473
|
runId: string;
|
|
466
474
|
userMessageId: string;
|
|
467
475
|
status: TFlexReversionSegmentStatus;
|
|
@@ -472,13 +480,25 @@ export interface IFlexReversionSegment {
|
|
|
472
480
|
workspaceReference?: TJsonValue;
|
|
473
481
|
}
|
|
474
482
|
|
|
475
|
-
export interface
|
|
483
|
+
export interface IFlexReversionSegment extends IFlexReversionSegmentV2 {
|
|
484
|
+
protocolVersion: TFlexReversionProtocolVersion;
|
|
485
|
+
provenance: TFlexReversionProvenance;
|
|
486
|
+
disposition?: TFlexReversionDisposition;
|
|
487
|
+
affectedWorkspaces?: IFlexAffectedWorkspace[];
|
|
488
|
+
reasonCode?: string;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
interface IFlexPendingCaptureReversionV2 {
|
|
476
492
|
kind: 'capture';
|
|
477
493
|
runId: string;
|
|
478
494
|
captureId: string;
|
|
479
495
|
state: 'preparing' | 'prepared' | 'finalizing';
|
|
480
496
|
}
|
|
481
497
|
|
|
498
|
+
export interface IFlexPendingCaptureReversion extends IFlexPendingCaptureReversionV2 {
|
|
499
|
+
protocolVersion: TFlexReversionProtocolVersion;
|
|
500
|
+
}
|
|
501
|
+
|
|
482
502
|
export interface IFlexPendingApplyReversion {
|
|
483
503
|
kind: 'apply';
|
|
484
504
|
operationId: string;
|
|
@@ -489,21 +509,41 @@ export interface IFlexPendingApplyReversion {
|
|
|
489
509
|
appliedRunIds: string[];
|
|
490
510
|
}
|
|
491
511
|
|
|
512
|
+
type TFlexPendingReversionV2 =
|
|
513
|
+
| IFlexPendingCaptureReversionV2
|
|
514
|
+
| IFlexPendingApplyReversion;
|
|
515
|
+
|
|
492
516
|
export type TFlexPendingReversion =
|
|
493
517
|
| IFlexPendingCaptureReversion
|
|
494
518
|
| IFlexPendingApplyReversion;
|
|
495
519
|
|
|
496
|
-
|
|
520
|
+
interface IFlexPendingReversionReleaseV2 {
|
|
497
521
|
runId: string;
|
|
498
522
|
captureId: string;
|
|
499
523
|
reference: TJsonValue;
|
|
500
524
|
}
|
|
501
525
|
|
|
526
|
+
export interface IFlexPendingReversionRelease extends IFlexPendingReversionReleaseV2 {
|
|
527
|
+
protocolVersion: TFlexReversionProtocolVersion;
|
|
528
|
+
}
|
|
529
|
+
|
|
502
530
|
export interface IFlexProjectionSnapshotV2 {
|
|
503
531
|
schemaVersion: 2;
|
|
504
532
|
revision: number;
|
|
505
533
|
messages: IFlexMessage[];
|
|
506
534
|
stagedTerminals: IFlexTerminalProjection[];
|
|
535
|
+
reversionSegments: IFlexReversionSegmentV2[];
|
|
536
|
+
revertCursor: number;
|
|
537
|
+
excludedRunIds: string[];
|
|
538
|
+
pendingReversion?: TFlexPendingReversionV2;
|
|
539
|
+
pendingReversionReleases: IFlexPendingReversionReleaseV2[];
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export interface IFlexProjectionSnapshotV3 {
|
|
543
|
+
schemaVersion: 3;
|
|
544
|
+
revision: number;
|
|
545
|
+
messages: IFlexMessage[];
|
|
546
|
+
stagedTerminals: IFlexTerminalProjection[];
|
|
507
547
|
reversionSegments: IFlexReversionSegment[];
|
|
508
548
|
revertCursor: number;
|
|
509
549
|
excludedRunIds: string[];
|
|
@@ -511,9 +551,12 @@ export interface IFlexProjectionSnapshotV2 {
|
|
|
511
551
|
pendingReversionReleases: IFlexPendingReversionRelease[];
|
|
512
552
|
}
|
|
513
553
|
|
|
514
|
-
export type IFlexProjectionSnapshot =
|
|
554
|
+
export type IFlexProjectionSnapshot =
|
|
555
|
+
| IFlexProjectionSnapshotV1
|
|
556
|
+
| IFlexProjectionSnapshotV2
|
|
557
|
+
| IFlexProjectionSnapshotV3;
|
|
515
558
|
export type TFlexProjectionSnapshot = IFlexProjectionSnapshot;
|
|
516
|
-
export type IFlexProjectionSnapshotCurrent =
|
|
559
|
+
export type IFlexProjectionSnapshotCurrent = IFlexProjectionSnapshotV3;
|
|
517
560
|
|
|
518
561
|
export interface IFlexPermissionSnapshot {
|
|
519
562
|
schemaVersion: 1;
|
|
@@ -619,6 +662,10 @@ export const FLEX_REVERSION_MAXIMUM_LIMITS = Object.freeze({
|
|
|
619
662
|
});
|
|
620
663
|
|
|
621
664
|
export const FLEX_REVERSION_REFERENCE_MAX_BYTES = 256 * 1024;
|
|
665
|
+
export const FLEX_REVERSION_MAX_AFFECTED_WORKSPACES = 64;
|
|
666
|
+
export const FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES = 512;
|
|
667
|
+
export const FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES = 2048;
|
|
668
|
+
export const FLEX_REVERSION_REASON_CODE_MAX_BYTES = 128;
|
|
622
669
|
|
|
623
670
|
export interface IFlexTurnReversionBaseContext<TScope> {
|
|
624
671
|
readonly scopeId: string;
|
|
@@ -672,6 +719,77 @@ export interface IFlexTurnReversionProvider<TScope> {
|
|
|
672
719
|
): Promise<void> | void;
|
|
673
720
|
}
|
|
674
721
|
|
|
722
|
+
export interface IFlexTurnReversionRevertibleOutcome {
|
|
723
|
+
readonly disposition: 'revertible';
|
|
724
|
+
readonly reference: TJsonValue;
|
|
725
|
+
readonly affectedWorkspaces: readonly IFlexAffectedWorkspace[];
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
export interface IFlexTurnReversionNoChangeOutcome {
|
|
729
|
+
readonly disposition: 'no-change';
|
|
730
|
+
readonly reference: TJsonValue;
|
|
731
|
+
readonly affectedWorkspaces?: readonly [];
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export interface IFlexTurnReversionNonrevertibleOutcome {
|
|
735
|
+
readonly disposition: 'nonrevertible';
|
|
736
|
+
readonly reference: TJsonValue;
|
|
737
|
+
readonly reasonCode: string;
|
|
738
|
+
readonly affectedWorkspaces?: readonly IFlexAffectedWorkspace[];
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export type TFlexTurnReversionFinalizedOutcome =
|
|
742
|
+
| IFlexTurnReversionRevertibleOutcome
|
|
743
|
+
| IFlexTurnReversionNoChangeOutcome
|
|
744
|
+
| IFlexTurnReversionNonrevertibleOutcome;
|
|
745
|
+
|
|
746
|
+
export type TFlexTurnReversionCaptureInspectionV2 =
|
|
747
|
+
| { readonly status: 'missing' | 'prepared' | 'unknown' }
|
|
748
|
+
| {
|
|
749
|
+
readonly status: 'finalized';
|
|
750
|
+
readonly outcome: TFlexTurnReversionFinalizedOutcome;
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
export interface IFlexTurnReversionProviderV2<TScope> {
|
|
754
|
+
readonly protocolVersion: 2;
|
|
755
|
+
prepare(
|
|
756
|
+
context: IFlexTurnReversionBaseContext<TScope>,
|
|
757
|
+
): Promise<void> | void;
|
|
758
|
+
inspectCapture(
|
|
759
|
+
context: IFlexTurnReversionBaseContext<TScope>,
|
|
760
|
+
): Promise<TFlexTurnReversionCaptureInspectionV2> | TFlexTurnReversionCaptureInspectionV2;
|
|
761
|
+
finalize(
|
|
762
|
+
context: IFlexTurnReversionBaseContext<TScope>,
|
|
763
|
+
): Promise<TFlexTurnReversionFinalizedOutcome> | TFlexTurnReversionFinalizedOutcome;
|
|
764
|
+
apply(
|
|
765
|
+
context: IFlexTurnReversionApplyContext<TScope>,
|
|
766
|
+
): Promise<void> | void;
|
|
767
|
+
inspectApply(
|
|
768
|
+
context: IFlexTurnReversionApplyContext<TScope>,
|
|
769
|
+
): Promise<IFlexTurnReversionApplyInspection> | IFlexTurnReversionApplyInspection;
|
|
770
|
+
release(
|
|
771
|
+
context: IFlexTurnReversionReleaseContext<TScope>,
|
|
772
|
+
): Promise<void> | void;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export type TFlexReversionPolicy = 'transcript-optional' | 'workspace-required';
|
|
776
|
+
|
|
777
|
+
export type TFlexSessionReversionGroupKind = 'candidate' | 'barrier' | 'no-change';
|
|
778
|
+
|
|
779
|
+
export interface IFlexSessionReversionGroup {
|
|
780
|
+
readonly runId: string;
|
|
781
|
+
readonly kind: TFlexSessionReversionGroupKind;
|
|
782
|
+
readonly visibility: 'visible' | 'hidden';
|
|
783
|
+
readonly affectedWorkspaces: readonly IFlexAffectedWorkspace[];
|
|
784
|
+
readonly affectedWorkspacesTruncated: boolean;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export interface IFlexSessionReversionInfo {
|
|
788
|
+
readonly undoAvailable: boolean;
|
|
789
|
+
readonly redoAvailable: boolean;
|
|
790
|
+
readonly groups: readonly IFlexSessionReversionGroup[];
|
|
791
|
+
}
|
|
792
|
+
|
|
675
793
|
export interface IFlexUndoSessionResult {
|
|
676
794
|
revertedRunId: string;
|
|
677
795
|
}
|
|
@@ -769,7 +887,8 @@ export interface IFlexHarnessOptions<TScope> {
|
|
|
769
887
|
callbackLimits?: IFlexCallbackLimits;
|
|
770
888
|
promptQueueLimits?: IFlexPromptQueueLimits;
|
|
771
889
|
reversionLimits?: IFlexReversionLimits;
|
|
772
|
-
turnReversionProvider?: IFlexTurnReversionProvider<TScope>;
|
|
890
|
+
turnReversionProvider?: IFlexTurnReversionProvider<TScope> | IFlexTurnReversionProviderV2<TScope>;
|
|
891
|
+
reversionPolicy?: TFlexReversionPolicy;
|
|
773
892
|
externalErrorProjector?: TFlexExternalErrorProjector;
|
|
774
893
|
subagents?: IFlexSubagentDefinition[];
|
|
775
894
|
slashCommands?: readonly TFlexSlashCommandRegistration<TScope>[];
|
package/ts/utils.json.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { FlexHarnessStoreFormatError, FlexHarnessValidationError } from './errors.js';
|
|
2
2
|
import {
|
|
3
|
+
FLEX_REVERSION_MAX_AFFECTED_WORKSPACES,
|
|
3
4
|
FLEX_REVERSION_MAXIMUM_LIMITS,
|
|
5
|
+
FLEX_REVERSION_REASON_CODE_MAX_BYTES,
|
|
4
6
|
FLEX_REVERSION_REFERENCE_MAX_BYTES,
|
|
7
|
+
FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES,
|
|
8
|
+
FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES,
|
|
5
9
|
} from './interfaces.js';
|
|
6
10
|
import type {
|
|
7
11
|
IFlexJsonLimits,
|
|
8
12
|
IFlexPermissionSnapshot,
|
|
9
13
|
IFlexProjectionSnapshotV1,
|
|
10
14
|
IFlexProjectionSnapshotV2,
|
|
15
|
+
IFlexProjectionSnapshotV3,
|
|
11
16
|
IFlexScopeSnapshot,
|
|
12
17
|
TFlexAgentModelMessage,
|
|
13
18
|
TJsonValue,
|
|
@@ -1148,18 +1153,25 @@ export function assertFlexProjectionSnapshotV2(
|
|
|
1148
1153
|
const fromCursor = Number(pending.fromCursor);
|
|
1149
1154
|
const toCursor = Number(pending.toCursor);
|
|
1150
1155
|
const direction = pending.direction as 'undo' | 'redo';
|
|
1151
|
-
if (fromCursor !== Number(snapshot.revertCursor) || toCursor !== fromCursor + (direction === 'undo' ? -1 : 1)) {
|
|
1152
|
-
throw new FlexHarnessStoreFormatError('Snapshot pending apply cursor movement is invalid.');
|
|
1153
|
-
}
|
|
1154
1156
|
const completed = segments.filter((segment) =>
|
|
1155
1157
|
(segment as Record<string, unknown>).status === 'completed');
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1158
|
+
if (
|
|
1159
|
+
fromCursor !== Number(snapshot.revertCursor)
|
|
1160
|
+
|| (direction === 'undo' && !(toCursor >= 0 && toCursor < fromCursor))
|
|
1161
|
+
|| (direction === 'redo' && !(toCursor > fromCursor && toCursor <= completed.length))
|
|
1162
|
+
) {
|
|
1163
|
+
throw new FlexHarnessStoreFormatError('Snapshot pending apply cursor movement is invalid.');
|
|
1164
|
+
}
|
|
1165
|
+
const rangeStart = Math.min(fromCursor, toCursor);
|
|
1166
|
+
const rangeEnd = Math.max(fromCursor, toCursor);
|
|
1167
|
+
const first = completed[rangeStart];
|
|
1168
|
+
if (!first) throw new FlexHarnessStoreFormatError('Snapshot pending apply has no target candidate.');
|
|
1169
|
+
const firstRunId = (first as Record<string, unknown>).runId;
|
|
1170
|
+
const start = rangeStart === 0
|
|
1171
|
+
? 0
|
|
1172
|
+
: segments.findIndex((segment) =>
|
|
1173
|
+
(segment as Record<string, unknown>).runId === firstRunId);
|
|
1174
|
+
const next = completed[rangeEnd];
|
|
1163
1175
|
const end = next
|
|
1164
1176
|
? segments.findIndex((segment) =>
|
|
1165
1177
|
(segment as Record<string, unknown>).runId === (next as Record<string, unknown>).runId)
|
|
@@ -1211,12 +1223,259 @@ export function assertFlexProjectionSnapshotV2(
|
|
|
1211
1223
|
}
|
|
1212
1224
|
}
|
|
1213
1225
|
|
|
1226
|
+
function requireBoundedString(value: unknown, path: string, maxBytes: number): string {
|
|
1227
|
+
const result = requireString(value, path);
|
|
1228
|
+
if (Buffer.byteLength(result, 'utf8') > maxBytes) {
|
|
1229
|
+
throw new FlexHarnessStoreFormatError(`${path} exceeds its byte limit.`);
|
|
1230
|
+
}
|
|
1231
|
+
return result;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function validateAffectedWorkspaces(value: unknown, path: string): void {
|
|
1235
|
+
if (!Array.isArray(value)) {
|
|
1236
|
+
throw new FlexHarnessStoreFormatError(`${path} must be an array.`);
|
|
1237
|
+
}
|
|
1238
|
+
if (value.length > FLEX_REVERSION_MAX_AFFECTED_WORKSPACES) {
|
|
1239
|
+
throw new FlexHarnessStoreFormatError(`${path} exceeds its item limit.`);
|
|
1240
|
+
}
|
|
1241
|
+
const ids = new Set<string>();
|
|
1242
|
+
for (let index = 0; index < value.length; index++) {
|
|
1243
|
+
const workspacePath = `${path}[${index}]`;
|
|
1244
|
+
const workspace = requireRecord(value[index], workspacePath);
|
|
1245
|
+
requireOnlyKeys(workspace, ['id', 'label'], workspacePath);
|
|
1246
|
+
const id = requireBoundedString(
|
|
1247
|
+
workspace.id,
|
|
1248
|
+
`${workspacePath}.id`,
|
|
1249
|
+
FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES,
|
|
1250
|
+
);
|
|
1251
|
+
if (!id.trim()) {
|
|
1252
|
+
throw new FlexHarnessStoreFormatError(`${workspacePath}.id must contain non-whitespace characters.`);
|
|
1253
|
+
}
|
|
1254
|
+
const label = requireBoundedString(
|
|
1255
|
+
workspace.label,
|
|
1256
|
+
`${workspacePath}.label`,
|
|
1257
|
+
FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES,
|
|
1258
|
+
);
|
|
1259
|
+
if (!label.trim()) {
|
|
1260
|
+
throw new FlexHarnessStoreFormatError(`${workspacePath}.label must contain non-whitespace characters.`);
|
|
1261
|
+
}
|
|
1262
|
+
if (ids.has(id)) {
|
|
1263
|
+
throw new FlexHarnessStoreFormatError(`${path} contains duplicate workspace "${id}".`);
|
|
1264
|
+
}
|
|
1265
|
+
ids.add(id);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
export function assertFlexProjectionSnapshotV3(
|
|
1270
|
+
value: unknown,
|
|
1271
|
+
): asserts value is IFlexProjectionSnapshotV3 {
|
|
1272
|
+
assertJsonSerializable(value, '$snapshot');
|
|
1273
|
+
const snapshot = requireRecord(value, '$snapshot');
|
|
1274
|
+
requireOnlyKeys(snapshot, [
|
|
1275
|
+
'schemaVersion',
|
|
1276
|
+
'revision',
|
|
1277
|
+
'messages',
|
|
1278
|
+
'stagedTerminals',
|
|
1279
|
+
'reversionSegments',
|
|
1280
|
+
'revertCursor',
|
|
1281
|
+
'excludedRunIds',
|
|
1282
|
+
'pendingReversion',
|
|
1283
|
+
'pendingReversionReleases',
|
|
1284
|
+
], '$snapshot');
|
|
1285
|
+
if (snapshot.schemaVersion !== 3) {
|
|
1286
|
+
throw new FlexHarnessStoreFormatError('Projection snapshot schemaVersion must be 3.');
|
|
1287
|
+
}
|
|
1288
|
+
if (!Array.isArray(snapshot.reversionSegments)) {
|
|
1289
|
+
throw new FlexHarnessStoreFormatError('Snapshot reversionSegments must be an array.');
|
|
1290
|
+
}
|
|
1291
|
+
const retainedCaptureIds = new Set<string>();
|
|
1292
|
+
for (let index = 0; index < snapshot.reversionSegments.length; index++) {
|
|
1293
|
+
const path = `$snapshot.reversionSegments[${index}]`;
|
|
1294
|
+
const segment = requireRecord(snapshot.reversionSegments[index], path);
|
|
1295
|
+
requireOnlyKeys(segment, [
|
|
1296
|
+
'runId',
|
|
1297
|
+
'userMessageId',
|
|
1298
|
+
'status',
|
|
1299
|
+
'contextAvailable',
|
|
1300
|
+
'eventIds',
|
|
1301
|
+
'workspaceCaptured',
|
|
1302
|
+
'captureId',
|
|
1303
|
+
'workspaceReference',
|
|
1304
|
+
'protocolVersion',
|
|
1305
|
+
'provenance',
|
|
1306
|
+
'disposition',
|
|
1307
|
+
'affectedWorkspaces',
|
|
1308
|
+
'reasonCode',
|
|
1309
|
+
], path);
|
|
1310
|
+
const protocolVersion = segment.protocolVersion;
|
|
1311
|
+
if (protocolVersion !== 1 && protocolVersion !== 2) {
|
|
1312
|
+
throw new FlexHarnessStoreFormatError(`${path}.protocolVersion is invalid.`);
|
|
1313
|
+
}
|
|
1314
|
+
if (segment.provenance !== 'transcript' && segment.provenance !== 'workspace') {
|
|
1315
|
+
throw new FlexHarnessStoreFormatError(`${path}.provenance is invalid.`);
|
|
1316
|
+
}
|
|
1317
|
+
if (segment.workspaceCaptured !== (segment.provenance === 'workspace')) {
|
|
1318
|
+
throw new FlexHarnessStoreFormatError(`${path}.workspaceCaptured does not match its provenance.`);
|
|
1319
|
+
}
|
|
1320
|
+
if (protocolVersion === 2 && segment.provenance !== 'workspace') {
|
|
1321
|
+
throw new FlexHarnessStoreFormatError(`${path} uses protocol 2 without workspace provenance.`);
|
|
1322
|
+
}
|
|
1323
|
+
if (protocolVersion === 1 && segment.affectedWorkspaces !== undefined) {
|
|
1324
|
+
throw new FlexHarnessStoreFormatError(`${path} protocol 1 contains affected workspace metadata.`);
|
|
1325
|
+
}
|
|
1326
|
+
const status = String(segment.status);
|
|
1327
|
+
const disposition = segment.disposition;
|
|
1328
|
+
const captureId = segment.captureId;
|
|
1329
|
+
if (captureId !== undefined) retainedCaptureIds.add(String(captureId));
|
|
1330
|
+
if (segment.affectedWorkspaces !== undefined) {
|
|
1331
|
+
validateAffectedWorkspaces(segment.affectedWorkspaces, `${path}.affectedWorkspaces`);
|
|
1332
|
+
}
|
|
1333
|
+
if (segment.reasonCode !== undefined) {
|
|
1334
|
+
const reasonCode = requireBoundedString(
|
|
1335
|
+
segment.reasonCode,
|
|
1336
|
+
`${path}.reasonCode`,
|
|
1337
|
+
FLEX_REVERSION_REASON_CODE_MAX_BYTES,
|
|
1338
|
+
);
|
|
1339
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(reasonCode)) {
|
|
1340
|
+
throw new FlexHarnessStoreFormatError(`${path}.reasonCode is invalid.`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (segment.provenance === 'transcript') {
|
|
1344
|
+
if (
|
|
1345
|
+
disposition !== undefined
|
|
1346
|
+
|| captureId !== undefined
|
|
1347
|
+
|| segment.workspaceReference !== undefined
|
|
1348
|
+
|| segment.affectedWorkspaces !== undefined
|
|
1349
|
+
|| segment.reasonCode !== undefined
|
|
1350
|
+
) throw new FlexHarnessStoreFormatError(`${path} transcript provenance contains workspace data.`);
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
if (status === 'capturing') {
|
|
1354
|
+
if (disposition !== 'pending' || captureId === undefined || segment.workspaceReference !== undefined) {
|
|
1355
|
+
throw new FlexHarnessStoreFormatError(`${path} has invalid pending workspace ownership.`);
|
|
1356
|
+
}
|
|
1357
|
+
if (segment.affectedWorkspaces !== undefined || segment.reasonCode !== undefined) {
|
|
1358
|
+
throw new FlexHarnessStoreFormatError(`${path} pending workspace capture has terminal metadata.`);
|
|
1359
|
+
}
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
if (protocolVersion === 1 && disposition !== 'revertible') {
|
|
1363
|
+
throw new FlexHarnessStoreFormatError(`${path} terminal protocol-1 workspace capture is not revertible.`);
|
|
1364
|
+
}
|
|
1365
|
+
if (protocolVersion === 2 && !['revertible', 'no-change', 'nonrevertible'].includes(String(disposition))) {
|
|
1366
|
+
throw new FlexHarnessStoreFormatError(`${path} terminal protocol-2 capture lacks a conclusive disposition.`);
|
|
1367
|
+
}
|
|
1368
|
+
if (disposition === 'revertible') {
|
|
1369
|
+
if (captureId === undefined || segment.workspaceReference === undefined) {
|
|
1370
|
+
throw new FlexHarnessStoreFormatError(`${path} revertible capture lacks retained ownership.`);
|
|
1371
|
+
}
|
|
1372
|
+
if (protocolVersion === 2 && segment.affectedWorkspaces === undefined) {
|
|
1373
|
+
throw new FlexHarnessStoreFormatError(`${path} revertible protocol-2 capture lacks affected workspaces.`);
|
|
1374
|
+
}
|
|
1375
|
+
if (segment.reasonCode !== undefined) {
|
|
1376
|
+
throw new FlexHarnessStoreFormatError(`${path} revertible capture has a reason code.`);
|
|
1377
|
+
}
|
|
1378
|
+
} else {
|
|
1379
|
+
if (captureId !== undefined || segment.workspaceReference !== undefined) {
|
|
1380
|
+
throw new FlexHarnessStoreFormatError(`${path} terminal disposition retains segment ownership.`);
|
|
1381
|
+
}
|
|
1382
|
+
if (disposition === 'no-change') {
|
|
1383
|
+
if (Array.isArray(segment.affectedWorkspaces) && segment.affectedWorkspaces.length > 0) {
|
|
1384
|
+
throw new FlexHarnessStoreFormatError(`${path} no-change capture affects a workspace.`);
|
|
1385
|
+
}
|
|
1386
|
+
if (segment.reasonCode !== undefined) {
|
|
1387
|
+
throw new FlexHarnessStoreFormatError(`${path} no-change capture has a reason code.`);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
if (disposition === 'nonrevertible' && segment.reasonCode === undefined) {
|
|
1391
|
+
throw new FlexHarnessStoreFormatError(`${path} nonrevertible capture lacks a reason code.`);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (snapshot.pendingReversion !== undefined) {
|
|
1396
|
+
const pending = requireRecord(snapshot.pendingReversion, '$snapshot.pendingReversion');
|
|
1397
|
+
if (pending.kind === 'capture') {
|
|
1398
|
+
requireOnlyKeys(
|
|
1399
|
+
pending,
|
|
1400
|
+
['kind', 'runId', 'captureId', 'state', 'protocolVersion'],
|
|
1401
|
+
'$snapshot.pendingReversion',
|
|
1402
|
+
);
|
|
1403
|
+
if (pending.protocolVersion !== 1 && pending.protocolVersion !== 2) {
|
|
1404
|
+
throw new FlexHarnessStoreFormatError('$snapshot.pendingReversion.protocolVersion is invalid.');
|
|
1405
|
+
}
|
|
1406
|
+
const matching = snapshot.reversionSegments.filter((entry) => {
|
|
1407
|
+
const segment = entry as Record<string, unknown>;
|
|
1408
|
+
return segment.runId === pending.runId
|
|
1409
|
+
&& segment.captureId === pending.captureId
|
|
1410
|
+
&& segment.protocolVersion === pending.protocolVersion
|
|
1411
|
+
&& segment.status === 'capturing'
|
|
1412
|
+
&& segment.disposition === 'pending';
|
|
1413
|
+
});
|
|
1414
|
+
if (matching.length !== 1) {
|
|
1415
|
+
throw new FlexHarnessStoreFormatError('Snapshot pending capture does not match exactly one segment.');
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (!Array.isArray(snapshot.pendingReversionReleases)) {
|
|
1420
|
+
throw new FlexHarnessStoreFormatError('Snapshot pendingReversionReleases must be an array.');
|
|
1421
|
+
}
|
|
1422
|
+
const releaseCaptureIds = new Set<string>();
|
|
1423
|
+
for (let index = 0; index < snapshot.pendingReversionReleases.length; index++) {
|
|
1424
|
+
const path = `$snapshot.pendingReversionReleases[${index}]`;
|
|
1425
|
+
const release = requireRecord(snapshot.pendingReversionReleases[index], path);
|
|
1426
|
+
requireOnlyKeys(release, ['runId', 'captureId', 'reference', 'protocolVersion'], path);
|
|
1427
|
+
const captureId = requireString(release.captureId, `${path}.captureId`);
|
|
1428
|
+
if (release.protocolVersion !== 1 && release.protocolVersion !== 2) {
|
|
1429
|
+
throw new FlexHarnessStoreFormatError(`${path}.protocolVersion is invalid.`);
|
|
1430
|
+
}
|
|
1431
|
+
if (releaseCaptureIds.has(captureId) || retainedCaptureIds.has(captureId)) {
|
|
1432
|
+
throw new FlexHarnessStoreFormatError(`${path} has duplicate durable capture ownership.`);
|
|
1433
|
+
}
|
|
1434
|
+
releaseCaptureIds.add(captureId);
|
|
1435
|
+
}
|
|
1436
|
+
const compatibleV2 = {
|
|
1437
|
+
...snapshot,
|
|
1438
|
+
schemaVersion: 2,
|
|
1439
|
+
reversionSegments: snapshot.reversionSegments.map((entry) => {
|
|
1440
|
+
const segment = entry as Record<string, unknown>;
|
|
1441
|
+
return {
|
|
1442
|
+
runId: segment.runId,
|
|
1443
|
+
userMessageId: segment.userMessageId,
|
|
1444
|
+
status: segment.status,
|
|
1445
|
+
contextAvailable: segment.contextAvailable,
|
|
1446
|
+
eventIds: segment.eventIds,
|
|
1447
|
+
workspaceCaptured: segment.provenance === 'workspace'
|
|
1448
|
+
&& (segment.status === 'capturing' || segment.disposition === 'revertible'),
|
|
1449
|
+
...(segment.captureId === undefined ? {} : { captureId: segment.captureId }),
|
|
1450
|
+
...(segment.workspaceReference === undefined
|
|
1451
|
+
? {}
|
|
1452
|
+
: { workspaceReference: segment.workspaceReference }),
|
|
1453
|
+
};
|
|
1454
|
+
}),
|
|
1455
|
+
...(snapshot.pendingReversion === undefined
|
|
1456
|
+
? {}
|
|
1457
|
+
: {
|
|
1458
|
+
pendingReversion: (() => {
|
|
1459
|
+
const { protocolVersion: _protocolVersion, ...pending } = snapshot.pendingReversion as
|
|
1460
|
+
Record<string, unknown>;
|
|
1461
|
+
return pending;
|
|
1462
|
+
})(),
|
|
1463
|
+
}),
|
|
1464
|
+
pendingReversionReleases: snapshot.pendingReversionReleases.map((entry) => {
|
|
1465
|
+
const { protocolVersion: _protocolVersion, ...release } = entry as Record<string, unknown>;
|
|
1466
|
+
return release;
|
|
1467
|
+
}),
|
|
1468
|
+
};
|
|
1469
|
+
assertFlexProjectionSnapshotV2(compatibleV2);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1214
1472
|
export function assertFlexProjectionSnapshot(
|
|
1215
1473
|
value: unknown,
|
|
1216
|
-
): asserts value is IFlexProjectionSnapshotV1 | IFlexProjectionSnapshotV2 {
|
|
1474
|
+
): asserts value is IFlexProjectionSnapshotV1 | IFlexProjectionSnapshotV2 | IFlexProjectionSnapshotV3 {
|
|
1217
1475
|
const record = requireRecord(value, '$snapshot');
|
|
1218
1476
|
if (record.schemaVersion === 1) assertFlexProjectionSnapshotV1(value);
|
|
1219
|
-
else assertFlexProjectionSnapshotV2(value);
|
|
1477
|
+
else if (record.schemaVersion === 2) assertFlexProjectionSnapshotV2(value);
|
|
1478
|
+
else assertFlexProjectionSnapshotV3(value);
|
|
1220
1479
|
}
|
|
1221
1480
|
|
|
1222
1481
|
export function assertFlexPermissionSnapshot(
|