@modelprofile.com/flexharness 9.4.0 → 9.6.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +33 -1
- package/dist_ts/classes.flexharness.js +415 -22
- package/dist_ts/errors.d.ts +5 -0
- package/dist_ts/errors.js +10 -1
- package/dist_ts/interfaces.d.ts +115 -0
- package/dist_ts/interfaces.js +19 -1
- package/dist_ts/utils.json.d.ts +5 -0
- package/dist_ts/utils.json.js +38 -1
- package/dist_ts/utils.sessionimport.d.ts +26 -0
- package/dist_ts/utils.sessionimport.js +241 -0
- package/package.json +3 -3
- package/readme.md +86 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +492 -18
- package/ts/errors.ts +11 -0
- package/ts/interfaces.ts +130 -0
- package/ts/readme.md +86 -3
- package/ts/utils.json.ts +41 -0
- package/ts/utils.sessionimport.ts +327 -0
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
FlexHarnessCallbackOverflowError,
|
|
5
5
|
FlexHarnessClosedError,
|
|
6
6
|
FlexHarnessExternalError,
|
|
7
|
+
FlexHarnessImportedSessionError,
|
|
7
8
|
FlexHarnessNotFoundError,
|
|
8
9
|
FlexHarnessPermissionRejectedError,
|
|
9
10
|
FlexHarnessPermissionStateError,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
FLEX_REVERSION_REFERENCE_MAX_BYTES,
|
|
26
27
|
FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES,
|
|
27
28
|
FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES,
|
|
29
|
+
FLEX_SESSION_IMPORT_MAX_DEPTH,
|
|
28
30
|
} from './interfaces.js';
|
|
29
31
|
import type {
|
|
30
32
|
IFlexAgentContextCompactionOptions,
|
|
@@ -32,6 +34,7 @@ import type {
|
|
|
32
34
|
IFlexAgentSessionPolicy,
|
|
33
35
|
IFlexAffectedWorkspace,
|
|
34
36
|
IFlexBackgroundExecution,
|
|
37
|
+
IFlexBeginSessionImportOptions,
|
|
35
38
|
IFlexCallbackLimits,
|
|
36
39
|
IFlexCompactSessionOptions,
|
|
37
40
|
IFlexCreateProjectTaskInput,
|
|
@@ -45,6 +48,7 @@ import type {
|
|
|
45
48
|
IFlexExecutionContextHandle,
|
|
46
49
|
IFlexHarnessOptions,
|
|
47
50
|
IFlexHarnessStores,
|
|
51
|
+
IFlexImportedRun,
|
|
48
52
|
IFlexMessage,
|
|
49
53
|
IFlexMessagePage,
|
|
50
54
|
IFlexMessagePageOptions,
|
|
@@ -85,6 +89,10 @@ import type {
|
|
|
85
89
|
IFlexSessionActivity,
|
|
86
90
|
IFlexSessionGeneration,
|
|
87
91
|
IFlexSessionGenerationCohortEntry,
|
|
92
|
+
IFlexSessionImportHandle,
|
|
93
|
+
IFlexSessionImportLimits,
|
|
94
|
+
IFlexSessionImportProgress,
|
|
95
|
+
IFlexSessionOrigin,
|
|
88
96
|
IFlexSessionReversionGroup,
|
|
89
97
|
IFlexSessionReversionInfo,
|
|
90
98
|
IFlexSessionTombstone,
|
|
@@ -106,6 +114,7 @@ import type {
|
|
|
106
114
|
TFlexAgentToolCallUpdateEvent,
|
|
107
115
|
TFlexAgentToolCallFinishEvent,
|
|
108
116
|
TFlexAgentToolSet,
|
|
117
|
+
TFlexBeginSessionImportResult,
|
|
109
118
|
TFlexExternalErrorProjector,
|
|
110
119
|
TFlexExternalErrorSource,
|
|
111
120
|
TFlexHarnessEvent,
|
|
@@ -184,6 +193,14 @@ import {
|
|
|
184
193
|
truncateUtf8,
|
|
185
194
|
type IRunResultProjection,
|
|
186
195
|
} from './utils.transfer.js';
|
|
196
|
+
import {
|
|
197
|
+
normalizeBeginSessionImportOptions,
|
|
198
|
+
normalizeImportedRunPage,
|
|
199
|
+
resolveSessionImportLimits,
|
|
200
|
+
sessionImportIdentity,
|
|
201
|
+
validateSessionImportHandle,
|
|
202
|
+
type INormalizedSessionImportOptions,
|
|
203
|
+
} from './utils.sessionimport.js';
|
|
187
204
|
import { wrapToolSet } from './utils.tooloutput.js';
|
|
188
205
|
import {
|
|
189
206
|
validateCompactSessionOptions,
|
|
@@ -299,6 +316,25 @@ interface IOrphanedExecutionContextOwner {
|
|
|
299
316
|
sessionId: string;
|
|
300
317
|
}
|
|
301
318
|
|
|
319
|
+
/** A staged session import: its runs stay in memory until the import is committed. */
|
|
320
|
+
interface IStagedSessionImport {
|
|
321
|
+
importId: string;
|
|
322
|
+
scopeId: string;
|
|
323
|
+
state: IStorageState;
|
|
324
|
+
scope: unknown;
|
|
325
|
+
sessionId: string;
|
|
326
|
+
options: INormalizedSessionImportOptions;
|
|
327
|
+
identity: string;
|
|
328
|
+
runs: IFlexImportedRun[];
|
|
329
|
+
runIds: Set<string>;
|
|
330
|
+
messageIds: Set<string>;
|
|
331
|
+
messageCount: number;
|
|
332
|
+
bytes: number;
|
|
333
|
+
phase: 'staging' | 'committing';
|
|
334
|
+
/** Discards the import when it stays idle; cleared once the import commits or is released. */
|
|
335
|
+
idleTimer?: ReturnType<typeof setTimeout>;
|
|
336
|
+
}
|
|
337
|
+
|
|
302
338
|
interface IStorageState {
|
|
303
339
|
storageKey: string;
|
|
304
340
|
compactorLifecycleController: AbortController;
|
|
@@ -310,6 +346,8 @@ interface IStorageState {
|
|
|
310
346
|
sessionDeletions: Map<string, Promise<void>>;
|
|
311
347
|
initializingSessions: Set<string>;
|
|
312
348
|
sessionInitializations: Map<string, Promise<void>>;
|
|
349
|
+
/** Staged session imports by session id; their ids are also in `initializingSessions`. */
|
|
350
|
+
sessionImports: Map<string, IStagedSessionImport>;
|
|
313
351
|
tombstones: Map<string, IFlexSessionTombstone>;
|
|
314
352
|
tombstoneCleanups: Map<string, Promise<void>>;
|
|
315
353
|
activeRuns: Map<string, IActiveRun>;
|
|
@@ -575,6 +613,7 @@ const resourceToolStemLength = 16;
|
|
|
575
613
|
const repairCancellationMessage = 'The process stopped before this run completed.';
|
|
576
614
|
const scopeRetirementMessage = 'The scope is being retired.';
|
|
577
615
|
const slashCommandSessionUnavailableReason = 'Session must be idle with no queued prompts.';
|
|
616
|
+
const importedSessionUnavailableReason = 'Session is an imported conversation and is read-only.';
|
|
578
617
|
const externalErrorFallback: IFlexErrorInfo = Object.freeze({
|
|
579
618
|
name: 'FlexHarnessExternalError',
|
|
580
619
|
message: 'The model operation failed.',
|
|
@@ -714,6 +753,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
714
753
|
private readonly callbackLimits: Required<IFlexCallbackLimits>;
|
|
715
754
|
private readonly promptQueueLimits: Required<IFlexPromptQueueLimits>;
|
|
716
755
|
private readonly reversionLimits: Required<IFlexReversionLimits>;
|
|
756
|
+
private readonly sessionImportLimits: Required<IFlexSessionImportLimits>;
|
|
717
757
|
private readonly reversionMaintenanceTimeoutMs: number;
|
|
718
758
|
private readonly turnReversionProvider: IFlexHarnessOptions<TScope>['turnReversionProvider'];
|
|
719
759
|
private readonly reversionPolicy: TFlexReversionPolicy;
|
|
@@ -743,6 +783,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
743
783
|
private readonly projectManagementQueues = new Map<string, Promise<void>>();
|
|
744
784
|
private readonly activeSlashCommandExecutions = new Map<string, IActiveSlashCommandExecution>();
|
|
745
785
|
private readonly activeSlashCommandListings = new Set<IActiveSlashCommandListing>();
|
|
786
|
+
private readonly stagedSessionImports = new Map<string, IStagedSessionImport>();
|
|
746
787
|
private readonly compactorInvocationContext = new plugins.AsyncLocalStorage<
|
|
747
788
|
ICompactorInvocation<TScope>
|
|
748
789
|
>();
|
|
@@ -779,6 +820,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
779
820
|
this.callbackLimits = resolveCallbackLimits(options.callbackLimits);
|
|
780
821
|
this.promptQueueLimits = resolvePromptQueueLimits(options.promptQueueLimits);
|
|
781
822
|
this.reversionLimits = resolveReversionLimits(options.reversionLimits);
|
|
823
|
+
this.sessionImportLimits = resolveSessionImportLimits(options.sessionImportLimits);
|
|
782
824
|
this.reversionMaintenanceTimeoutMs = resolveReversionMaintenanceTimeoutMs(options);
|
|
783
825
|
this.turnReversionProvider = options.turnReversionProvider;
|
|
784
826
|
this.reversionPolicy = options.reversionPolicy ?? 'transcript-optional';
|
|
@@ -996,6 +1038,272 @@ export class FlexHarness<TScope = unknown> {
|
|
|
996
1038
|
return result;
|
|
997
1039
|
}
|
|
998
1040
|
|
|
1041
|
+
/**
|
|
1042
|
+
* Starts a staged import of a finished conversation into a new read-only session. The session
|
|
1043
|
+
* stays hidden until `commitSessionImport()`. A committed session with the same source, source
|
|
1044
|
+
* id and part is reported instead of being imported twice.
|
|
1045
|
+
*/
|
|
1046
|
+
public async beginSessionImport(
|
|
1047
|
+
scopeId: string,
|
|
1048
|
+
options: IFlexBeginSessionImportOptions,
|
|
1049
|
+
): Promise<TFlexBeginSessionImportResult> {
|
|
1050
|
+
this.assertOpen();
|
|
1051
|
+
const normalized = normalizeBeginSessionImportOptions(options);
|
|
1052
|
+
const { scope, state } = await this.resolveState(scopeId);
|
|
1053
|
+
await state.scopeQueue;
|
|
1054
|
+
this.assertOpen();
|
|
1055
|
+
this.assertStateAcceptingWork(state);
|
|
1056
|
+
const { sessionId } = normalized;
|
|
1057
|
+
const identity = sessionImportIdentity(normalized.origin);
|
|
1058
|
+
for (const staged of state.sessionImports.values()) {
|
|
1059
|
+
if (staged.sessionId === sessionId || staged.identity === identity) {
|
|
1060
|
+
throw new FlexHarnessSessionBusyError(staged.sessionId, 'is already being imported');
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
for (const stored of state.sessions.values()) {
|
|
1064
|
+
const origin = stored.session.origin;
|
|
1065
|
+
if (origin === undefined || sessionImportIdentity(origin) !== identity) continue;
|
|
1066
|
+
const existing: TFlexBeginSessionImportResult = {
|
|
1067
|
+
status: origin.digest === normalized.origin.digest ? 'already_imported' : 'digest_mismatch',
|
|
1068
|
+
session: stored.session,
|
|
1069
|
+
};
|
|
1070
|
+
return publicSnapshot(existing);
|
|
1071
|
+
}
|
|
1072
|
+
if (
|
|
1073
|
+
state.sessions.has(sessionId)
|
|
1074
|
+
|| state.tombstones.has(sessionId)
|
|
1075
|
+
|| state.initializingSessions.has(sessionId)
|
|
1076
|
+
) {
|
|
1077
|
+
throw new FlexHarnessValidationError(`Session "${sessionId}" already exists or is being deleted.`);
|
|
1078
|
+
}
|
|
1079
|
+
if (normalized.parent) this.requireImportParent(state, sessionId, normalized.parent);
|
|
1080
|
+
if (this.stagedSessionImports.size >= this.sessionImportLimits.maxOpenImports) {
|
|
1081
|
+
throw new FlexHarnessQueueFullError('FlexHarness has reached its open session import limit.');
|
|
1082
|
+
}
|
|
1083
|
+
const staged: IStagedSessionImport = {
|
|
1084
|
+
importId: `import_${plugins.crypto.randomUUID()}`,
|
|
1085
|
+
scopeId,
|
|
1086
|
+
state,
|
|
1087
|
+
scope: scope.scope,
|
|
1088
|
+
sessionId,
|
|
1089
|
+
options: normalized,
|
|
1090
|
+
identity,
|
|
1091
|
+
runs: [],
|
|
1092
|
+
runIds: new Set(),
|
|
1093
|
+
messageIds: new Set(),
|
|
1094
|
+
messageCount: 0,
|
|
1095
|
+
bytes: 0,
|
|
1096
|
+
phase: 'staging',
|
|
1097
|
+
};
|
|
1098
|
+
this.stagedSessionImports.set(staged.importId, staged);
|
|
1099
|
+
this.armStagedImportIdleTimer(staged);
|
|
1100
|
+
state.sessionImports.set(sessionId, staged);
|
|
1101
|
+
state.initializingSessions.add(sessionId);
|
|
1102
|
+
const started: TFlexBeginSessionImportResult = {
|
|
1103
|
+
status: 'started',
|
|
1104
|
+
handle: { importId: staged.importId, scopeId, sessionId },
|
|
1105
|
+
};
|
|
1106
|
+
return publicSnapshot(started);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/** Stages one page of runs, in order, for a started session import. A rejected page stages nothing. */
|
|
1110
|
+
public async appendImportedRuns(
|
|
1111
|
+
handle: IFlexSessionImportHandle,
|
|
1112
|
+
runs: readonly IFlexImportedRun[],
|
|
1113
|
+
): Promise<IFlexSessionImportProgress> {
|
|
1114
|
+
const staged = this.requireStagedSessionImport(handle);
|
|
1115
|
+
this.assertOpen();
|
|
1116
|
+
this.assertStateAcceptingWork(staged.state);
|
|
1117
|
+
const page = normalizeImportedRunPage(runs, staged.sessionId, this.sessionImportLimits.maxPageBytes);
|
|
1118
|
+
const runIds = new Set<string>();
|
|
1119
|
+
const messageIds = new Set<string>();
|
|
1120
|
+
for (const run of page.runs) {
|
|
1121
|
+
if (staged.runIds.has(run.user.runId) || runIds.has(run.user.runId)) {
|
|
1122
|
+
throw new FlexHarnessValidationError(`Imported run "${run.user.runId}" is not unique.`);
|
|
1123
|
+
}
|
|
1124
|
+
runIds.add(run.user.runId);
|
|
1125
|
+
for (const message of [run.user, run.assistant]) {
|
|
1126
|
+
if (staged.messageIds.has(message.messageId) || messageIds.has(message.messageId)) {
|
|
1127
|
+
throw new FlexHarnessValidationError(`Imported message "${message.messageId}" is not unique.`);
|
|
1128
|
+
}
|
|
1129
|
+
messageIds.add(message.messageId);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
const messageCount = staged.messageCount + page.runs.length * 2;
|
|
1133
|
+
if (messageCount > this.sessionImportLimits.maxMessagesPerSession) {
|
|
1134
|
+
throw new FlexHarnessValidationError(
|
|
1135
|
+
`An imported session holds at most ${this.sessionImportLimits.maxMessagesPerSession} messages.`,
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
const bytes = staged.bytes + page.bytes;
|
|
1139
|
+
if (bytes > this.sessionImportLimits.maxSessionBytes) {
|
|
1140
|
+
throw new FlexHarnessValidationError(
|
|
1141
|
+
`An imported session holds at most ${this.sessionImportLimits.maxSessionBytes} message bytes.`,
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
staged.runs.push(...page.runs);
|
|
1145
|
+
for (const runId of runIds) staged.runIds.add(runId);
|
|
1146
|
+
for (const messageId of messageIds) staged.messageIds.add(messageId);
|
|
1147
|
+
staged.messageCount = messageCount;
|
|
1148
|
+
staged.bytes = bytes;
|
|
1149
|
+
this.armStagedImportIdleTimer(staged);
|
|
1150
|
+
return publicSnapshot({ runs: staged.runs.length, messages: messageCount, bytes });
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Persists the staged runs and publishes the session: archived, read-only and marked with its
|
|
1155
|
+
* `origin`. A failed commit removes everything it persisted before it rejects.
|
|
1156
|
+
*/
|
|
1157
|
+
public async commitSessionImport(handle: IFlexSessionImportHandle): Promise<IFlexSession> {
|
|
1158
|
+
const staged = this.requireStagedSessionImport(handle);
|
|
1159
|
+
this.assertOpen();
|
|
1160
|
+
const { state, scopeId, sessionId, options } = staged;
|
|
1161
|
+
this.assertStateAcceptingWork(state);
|
|
1162
|
+
if (staged.runs.length === 0) {
|
|
1163
|
+
throw new FlexHarnessValidationError('A session import needs at least one run before it is committed.');
|
|
1164
|
+
}
|
|
1165
|
+
staged.phase = 'committing';
|
|
1166
|
+
this.clearStagedImportIdleTimer(staged);
|
|
1167
|
+
const scope = staged.scope as TScope;
|
|
1168
|
+
let resolveInitialization!: () => void;
|
|
1169
|
+
const initialization = new Promise<void>((resolve) => {
|
|
1170
|
+
resolveInitialization = resolve;
|
|
1171
|
+
});
|
|
1172
|
+
let initializationCompleted = false;
|
|
1173
|
+
const completeInitialization = () => {
|
|
1174
|
+
if (initializationCompleted) return;
|
|
1175
|
+
initializationCompleted = true;
|
|
1176
|
+
if (state.sessionInitializations.get(sessionId) === initialization) {
|
|
1177
|
+
state.sessionInitializations.delete(sessionId);
|
|
1178
|
+
}
|
|
1179
|
+
resolveInitialization();
|
|
1180
|
+
};
|
|
1181
|
+
state.sessionInitializations.set(sessionId, initialization);
|
|
1182
|
+
const importedAt = new Date().toISOString();
|
|
1183
|
+
const origin: IFlexSessionOrigin = {
|
|
1184
|
+
kind: 'import',
|
|
1185
|
+
...cloneSerializable(options.origin),
|
|
1186
|
+
importedAt,
|
|
1187
|
+
};
|
|
1188
|
+
let metadata!: IFlexSession;
|
|
1189
|
+
let reserved = false;
|
|
1190
|
+
let loaded: IStoredSessionState | undefined;
|
|
1191
|
+
try {
|
|
1192
|
+
// A tombstone reserves the id while the projection is written, so a crash before the session
|
|
1193
|
+
// is published leaves only data that the next load or deletion cleans up.
|
|
1194
|
+
await this.mutateScope(state, () => {
|
|
1195
|
+
if (state.sessions.has(sessionId) || state.tombstones.has(sessionId)) {
|
|
1196
|
+
throw this.trustInternalError(new FlexHarnessValidationError(
|
|
1197
|
+
`Session "${sessionId}" already exists or is being deleted.`,
|
|
1198
|
+
));
|
|
1199
|
+
}
|
|
1200
|
+
const parent = options.parent
|
|
1201
|
+
? this.requireImportParent(state, sessionId, options.parent)
|
|
1202
|
+
: undefined;
|
|
1203
|
+
metadata = {
|
|
1204
|
+
scopeId,
|
|
1205
|
+
sessionId,
|
|
1206
|
+
sessionGenerationId: createSessionGenerationId(),
|
|
1207
|
+
sessionGenerationSequence: state.revision + 1,
|
|
1208
|
+
...(options.title === undefined ? {} : { title: options.title }),
|
|
1209
|
+
createdAt: options.createdAt,
|
|
1210
|
+
updatedAt: options.updatedAt,
|
|
1211
|
+
archivedAt: importedAt,
|
|
1212
|
+
status: 'idle',
|
|
1213
|
+
activity: { status: 'idle' },
|
|
1214
|
+
...(options.parent === undefined
|
|
1215
|
+
? {}
|
|
1216
|
+
: {
|
|
1217
|
+
parentSessionId: options.parent.sessionId,
|
|
1218
|
+
parentRunId: options.parent.runId,
|
|
1219
|
+
parentToolCallId: options.parent.toolCallId,
|
|
1220
|
+
agent: options.parent.agent,
|
|
1221
|
+
}),
|
|
1222
|
+
depth: parent === undefined ? 0 : (parent.session.depth ?? 0) + 1,
|
|
1223
|
+
origin,
|
|
1224
|
+
};
|
|
1225
|
+
state.tombstones.set(sessionId, {
|
|
1226
|
+
sessionId,
|
|
1227
|
+
...requireSessionGeneration(metadata),
|
|
1228
|
+
deletedAt: importedAt,
|
|
1229
|
+
rootSessionId: sessionId,
|
|
1230
|
+
depth: 0,
|
|
1231
|
+
imported: true,
|
|
1232
|
+
});
|
|
1233
|
+
});
|
|
1234
|
+
reserved = true;
|
|
1235
|
+
await this.stores.projections.save(state.storageKey, sessionId, {
|
|
1236
|
+
schemaVersion: 3,
|
|
1237
|
+
revision: 1,
|
|
1238
|
+
messages: staged.runs.flatMap((run) => [run.user, run.assistant]),
|
|
1239
|
+
stagedTerminals: [],
|
|
1240
|
+
reversionSegments: [],
|
|
1241
|
+
revertCursor: 0,
|
|
1242
|
+
excludedRunIds: [],
|
|
1243
|
+
pendingReversionReleases: [],
|
|
1244
|
+
}, 0);
|
|
1245
|
+
const runtime = await this.loadSessionRuntime(state, metadata, scopeId, scope);
|
|
1246
|
+
loaded = runtime;
|
|
1247
|
+
await this.mutateScope(state, () => {
|
|
1248
|
+
if (
|
|
1249
|
+
state.sessions.has(sessionId)
|
|
1250
|
+
|| state.tombstones.get(sessionId)?.sessionGenerationId !== metadata.sessionGenerationId
|
|
1251
|
+
) {
|
|
1252
|
+
throw this.trustInternalError(new FlexHarnessAbortError(
|
|
1253
|
+
'The session import lost its reservation before it was committed.',
|
|
1254
|
+
));
|
|
1255
|
+
}
|
|
1256
|
+
if (options.parent) this.requireImportParent(state, sessionId, options.parent);
|
|
1257
|
+
state.tombstones.delete(sessionId);
|
|
1258
|
+
state.sessions.set(sessionId, runtime);
|
|
1259
|
+
});
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
const projected = this.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-import');
|
|
1262
|
+
const errors: unknown[] = [projected];
|
|
1263
|
+
if (loaded && state.sessions.get(sessionId) !== loaded) {
|
|
1264
|
+
try {
|
|
1265
|
+
await this.closeStoredSession(loaded);
|
|
1266
|
+
} catch (closeError) {
|
|
1267
|
+
this.orphanedStoredSessions.add(loaded);
|
|
1268
|
+
errors.push(closeError);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
// A fenced namespace cannot tell whether its last save was persisted; its next load resolves
|
|
1272
|
+
// the reservation instead: a surviving tombstone is cleaned up, a published session is kept.
|
|
1273
|
+
if (reserved && state.tombstones.has(sessionId) && state.lifecycle !== 'fenced') {
|
|
1274
|
+
completeInitialization();
|
|
1275
|
+
try {
|
|
1276
|
+
await this.finishTombstoneCleanup(state, sessionId, scopeId, { scopeId, scope });
|
|
1277
|
+
} catch (cleanupError) {
|
|
1278
|
+
errors.push(this.projectOperationError(
|
|
1279
|
+
cleanupError,
|
|
1280
|
+
'persistence',
|
|
1281
|
+
scopeId,
|
|
1282
|
+
sessionId,
|
|
1283
|
+
'session-import-cleanup',
|
|
1284
|
+
));
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
throw combineErrors(errors);
|
|
1288
|
+
} finally {
|
|
1289
|
+
this.releaseStagedSessionImport(staged);
|
|
1290
|
+
completeInitialization();
|
|
1291
|
+
}
|
|
1292
|
+
const result = publicSnapshot(metadata);
|
|
1293
|
+
this.emitEvent(scopeId, metadata, { type: 'session.created', session: result });
|
|
1294
|
+
return result;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/** Discards a staged import. Nothing of it was persisted; an unknown or finished handle is ignored. */
|
|
1298
|
+
public async abortSessionImport(handle: IFlexSessionImportHandle): Promise<void> {
|
|
1299
|
+
const staged = this.findStagedSessionImport(handle);
|
|
1300
|
+
if (!staged) return;
|
|
1301
|
+
if (staged.phase === 'committing') {
|
|
1302
|
+
throw new FlexHarnessSessionBusyError(staged.sessionId, 'is being committed');
|
|
1303
|
+
}
|
|
1304
|
+
this.releaseStagedSessionImport(staged);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
999
1307
|
public async getSession(scopeId: string, sessionId: string): Promise<IFlexSession> {
|
|
1000
1308
|
const { state } = await this.resolveState(scopeId);
|
|
1001
1309
|
await state.scopeQueue;
|
|
@@ -1068,7 +1376,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1068
1376
|
scopeId: string,
|
|
1069
1377
|
sessionId: string,
|
|
1070
1378
|
): Promise<IFlexProjectStateResult> {
|
|
1071
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1379
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
1072
1380
|
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1073
1381
|
return this.projectStateResult(snapshot);
|
|
1074
1382
|
}
|
|
@@ -1077,7 +1385,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1077
1385
|
scopeId: string,
|
|
1078
1386
|
sessionId: string,
|
|
1079
1387
|
): Promise<IFlexProjectTasksResult> {
|
|
1080
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1388
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
1081
1389
|
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1082
1390
|
return this.projectTasksResult(snapshot, snapshot.tasks);
|
|
1083
1391
|
}
|
|
@@ -1088,7 +1396,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1088
1396
|
input: IFlexCreateProjectTaskInput,
|
|
1089
1397
|
): Promise<IFlexProjectTaskResult> {
|
|
1090
1398
|
validateCreateProjectTaskInput(input);
|
|
1091
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1399
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1092
1400
|
return this.createProjectTaskInState(
|
|
1093
1401
|
state,
|
|
1094
1402
|
stored,
|
|
@@ -1103,7 +1411,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1103
1411
|
input: IFlexUpdateProjectTaskInput,
|
|
1104
1412
|
): Promise<IFlexProjectTaskResult> {
|
|
1105
1413
|
validateUpdateProjectTaskInput(input);
|
|
1106
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1414
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1107
1415
|
return this.updateProjectTaskInState(
|
|
1108
1416
|
state,
|
|
1109
1417
|
stored,
|
|
@@ -1118,7 +1426,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1118
1426
|
taskId: string,
|
|
1119
1427
|
): Promise<IFlexProjectTaskResult> {
|
|
1120
1428
|
validateProjectTaskId(taskId);
|
|
1121
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1429
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1122
1430
|
return this.deleteProjectTaskInState(
|
|
1123
1431
|
state,
|
|
1124
1432
|
stored,
|
|
@@ -1131,7 +1439,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1131
1439
|
scopeId: string,
|
|
1132
1440
|
sessionId: string,
|
|
1133
1441
|
): Promise<IFlexProjectTasksResult> {
|
|
1134
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1442
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1135
1443
|
return this.clearProjectTasksInState(
|
|
1136
1444
|
state,
|
|
1137
1445
|
stored,
|
|
@@ -1143,7 +1451,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1143
1451
|
scopeId: string,
|
|
1144
1452
|
sessionId: string,
|
|
1145
1453
|
): Promise<IFlexProjectGoalResult> {
|
|
1146
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1454
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
1147
1455
|
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1148
1456
|
return this.projectGoalResult(snapshot);
|
|
1149
1457
|
}
|
|
@@ -1159,7 +1467,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1159
1467
|
FLEX_PROJECT_MANAGEMENT_LIMITS.maxGoalBytes,
|
|
1160
1468
|
true,
|
|
1161
1469
|
);
|
|
1162
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1470
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1163
1471
|
return this.setProjectGoalInState(
|
|
1164
1472
|
state,
|
|
1165
1473
|
stored,
|
|
@@ -1172,7 +1480,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1172
1480
|
scopeId: string,
|
|
1173
1481
|
sessionId: string,
|
|
1174
1482
|
): Promise<IFlexProjectGoalResult> {
|
|
1175
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1483
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1176
1484
|
return this.clearProjectGoalInState(
|
|
1177
1485
|
state,
|
|
1178
1486
|
stored,
|
|
@@ -1184,7 +1492,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1184
1492
|
scopeId: string,
|
|
1185
1493
|
sessionId: string,
|
|
1186
1494
|
): Promise<IFlexProjectScratchpadResult> {
|
|
1187
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1495
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
1188
1496
|
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1189
1497
|
return this.projectScratchpadResult(snapshot);
|
|
1190
1498
|
}
|
|
@@ -1199,7 +1507,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1199
1507
|
'project scratchpad',
|
|
1200
1508
|
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
1201
1509
|
);
|
|
1202
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1510
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1203
1511
|
return this.setProjectScratchpadInState(
|
|
1204
1512
|
state,
|
|
1205
1513
|
stored,
|
|
@@ -1218,7 +1526,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1218
1526
|
'project scratchpad append',
|
|
1219
1527
|
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
1220
1528
|
);
|
|
1221
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1529
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1222
1530
|
return this.appendProjectScratchpadInState(
|
|
1223
1531
|
state,
|
|
1224
1532
|
stored,
|
|
@@ -1231,7 +1539,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1231
1539
|
scopeId: string,
|
|
1232
1540
|
sessionId: string,
|
|
1233
1541
|
): Promise<IFlexProjectScratchpadResult> {
|
|
1234
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1542
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1235
1543
|
return this.clearProjectScratchpadInState(
|
|
1236
1544
|
state,
|
|
1237
1545
|
stored,
|
|
@@ -1295,6 +1603,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1295
1603
|
if (!tombstone || !matchesSessionGeneration(tombstone, validated.root)) {
|
|
1296
1604
|
return { matched: false };
|
|
1297
1605
|
}
|
|
1606
|
+
this.assertNoCommittingSessionImport(state, validated.root.sessionId);
|
|
1298
1607
|
const rootSessionId = tombstone.rootSessionId ?? tombstone.sessionId;
|
|
1299
1608
|
const descendantRoots = this.descendantTombstoneRoots(state, rootSessionId);
|
|
1300
1609
|
this.assertAuthorizedSessionGenerationCohort(
|
|
@@ -1323,6 +1632,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1323
1632
|
this.assertNoAmbientSlashCommandTeardown({ storageKey: state.storageKey });
|
|
1324
1633
|
validateIdentifier(sessionId, 'sessionId');
|
|
1325
1634
|
await state.scopeQueue;
|
|
1635
|
+
this.assertNoCommittingSessionImport(state, sessionId);
|
|
1326
1636
|
return this.startSessionDeletion(state, scopeId, scope.scope, sessionId);
|
|
1327
1637
|
}
|
|
1328
1638
|
|
|
@@ -1429,6 +1739,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1429
1739
|
? {}
|
|
1430
1740
|
: { parentSessionId: entry.session.parentSessionId }),
|
|
1431
1741
|
...(subagent === undefined ? {} : { subagent: cloneSerializable(subagent) }),
|
|
1742
|
+
...(entry.session.origin === undefined ? {} : { imported: true as const }),
|
|
1432
1743
|
});
|
|
1433
1744
|
}
|
|
1434
1745
|
return {
|
|
@@ -1597,7 +1908,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1597
1908
|
const nextCursor = index > 0
|
|
1598
1909
|
? this.createMessageCursor(namespace, sessionId, candidate.messageId)
|
|
1599
1910
|
: undefined;
|
|
1600
|
-
|
|
1911
|
+
const candidatePage: IFlexMessagePage = {
|
|
1912
|
+
messages: candidateMessages,
|
|
1913
|
+
startIndex: index,
|
|
1914
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
1915
|
+
};
|
|
1916
|
+
if (jsonBytes(candidatePage) > maxTransferPageBytes) break;
|
|
1601
1917
|
start = index;
|
|
1602
1918
|
messages = candidateMessages;
|
|
1603
1919
|
}
|
|
@@ -1607,7 +1923,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1607
1923
|
const nextCursor = start > 0
|
|
1608
1924
|
? this.createMessageCursor(namespace, sessionId, messages[0].messageId)
|
|
1609
1925
|
: undefined;
|
|
1610
|
-
return publicSnapshot({ messages, ...(nextCursor ? { nextCursor } : {}) });
|
|
1926
|
+
return publicSnapshot({ messages, startIndex: start, ...(nextCursor ? { nextCursor } : {}) });
|
|
1611
1927
|
}
|
|
1612
1928
|
|
|
1613
1929
|
public async getMessage(
|
|
@@ -1644,12 +1960,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1644
1960
|
signal,
|
|
1645
1961
|
);
|
|
1646
1962
|
signal.throwIfAborted();
|
|
1647
|
-
|
|
1963
|
+
const descriptors = this.createSlashCommandDescriptors(
|
|
1648
1964
|
state,
|
|
1649
1965
|
stored,
|
|
1650
1966
|
sessionAvailable,
|
|
1651
1967
|
reversionIdleReason,
|
|
1652
1968
|
);
|
|
1969
|
+
if (stored.session.origin === undefined) return descriptors;
|
|
1970
|
+
return publicSnapshot(descriptors.map((descriptor): IFlexSlashCommandDescriptor => ({
|
|
1971
|
+
...descriptor,
|
|
1972
|
+
available: false,
|
|
1973
|
+
unavailableReason: importedSessionUnavailableReason,
|
|
1974
|
+
})));
|
|
1653
1975
|
});
|
|
1654
1976
|
}
|
|
1655
1977
|
|
|
@@ -1788,6 +2110,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1788
2110
|
this.assertOpen();
|
|
1789
2111
|
this.assertStateAcceptingWork(state);
|
|
1790
2112
|
const stored = this.requireSession(state, sessionId);
|
|
2113
|
+
this.assertSessionWritable(stored);
|
|
1791
2114
|
const sessionKey = this.slashCommandSessionKey(state.storageKey, sessionId);
|
|
1792
2115
|
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
1793
2116
|
if (invocationOwner?.sessionKey === sessionKey) {
|
|
@@ -2383,6 +2706,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2383
2706
|
}
|
|
2384
2707
|
const { scope, state } = await this.resolveState(scopeId);
|
|
2385
2708
|
const stored = this.requireSession(state, sessionId);
|
|
2709
|
+
this.assertSessionWritable(stored);
|
|
2386
2710
|
this.assertStateAcceptingWork(state);
|
|
2387
2711
|
let canonical: plugins.TAgentToolExecutionReconciliationOptions;
|
|
2388
2712
|
if (reconciliation.resolution === 'executed') {
|
|
@@ -2422,6 +2746,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2422
2746
|
this.validateRuntimeEvent(event);
|
|
2423
2747
|
const { scope, state } = await this.resolveState(scopeId);
|
|
2424
2748
|
const stored = this.requireSession(state, sessionId);
|
|
2749
|
+
this.assertSessionWritable(stored);
|
|
2425
2750
|
this.assertStateAcceptingWork(state);
|
|
2426
2751
|
try {
|
|
2427
2752
|
await this.withCompactorContext(
|
|
@@ -2441,6 +2766,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2441
2766
|
const { modelHint } = validateCompactSessionOptions(options);
|
|
2442
2767
|
const { scope, state } = await this.resolveState(scopeId);
|
|
2443
2768
|
const stored = this.requireSession(state, sessionId);
|
|
2769
|
+
this.assertSessionWritable(stored);
|
|
2444
2770
|
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
2445
2771
|
if (invocationOwner?.sessionKey === this.slashCommandSessionKey(state.storageKey, sessionId)) {
|
|
2446
2772
|
throw this.trustInternalError(new FlexHarnessSlashCommandReentryError(sessionId));
|
|
@@ -2688,6 +3014,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2688
3014
|
signal?: AbortSignal,
|
|
2689
3015
|
): Promise<string> {
|
|
2690
3016
|
const stored = this.requireSession(state, sessionId);
|
|
3017
|
+
this.assertSessionWritable(stored);
|
|
2691
3018
|
this.assertStateAcceptingWork(state);
|
|
2692
3019
|
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
2693
3020
|
if (invocationOwner?.sessionKey === this.slashCommandSessionKey(state.storageKey, sessionId)) {
|
|
@@ -3040,6 +3367,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3040
3367
|
}
|
|
3041
3368
|
const { scope, state } = await this.resolveState(scopeId);
|
|
3042
3369
|
const stored = this.requireSession(state, sessionId);
|
|
3370
|
+
this.assertSessionWritable(stored);
|
|
3043
3371
|
this.assertStateAcceptingWork(state);
|
|
3044
3372
|
this.requireSessionAvailableForSlashCommand(state, stored, 'archive');
|
|
3045
3373
|
const compaction = compactionEventId === undefined
|
|
@@ -3239,6 +3567,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3239
3567
|
}
|
|
3240
3568
|
this.assertStateAcceptingWork(state);
|
|
3241
3569
|
const stored = this.requireSession(state, sessionId);
|
|
3570
|
+
this.assertSessionWritable(stored);
|
|
3242
3571
|
const sessionKey = this.slashCommandSessionKey(state.storageKey, sessionId);
|
|
3243
3572
|
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
3244
3573
|
if (invocationOwner?.sessionKey === sessionKey) {
|
|
@@ -4209,13 +4538,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4209
4538
|
private async resolveProjectManagementSession(
|
|
4210
4539
|
scopeId: string,
|
|
4211
4540
|
sessionId: string,
|
|
4541
|
+
access: 'read' | 'write',
|
|
4212
4542
|
): Promise<{ state: IStorageState; stored: IStoredSessionState }> {
|
|
4213
4543
|
this.requireProjectManagementTools();
|
|
4214
4544
|
const { state } = await this.resolveState(scopeId);
|
|
4215
4545
|
await state.scopeQueue;
|
|
4216
4546
|
this.assertOpen();
|
|
4217
4547
|
this.assertStateAcceptingWork(state);
|
|
4218
|
-
|
|
4548
|
+
const stored = this.requireSession(state, sessionId);
|
|
4549
|
+
if (access === 'write') this.assertSessionWritable(stored);
|
|
4550
|
+
return { state, stored };
|
|
4219
4551
|
}
|
|
4220
4552
|
|
|
4221
4553
|
private assertProjectManagementSession(
|
|
@@ -7984,6 +8316,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7984
8316
|
sessionDeletions: new Map(),
|
|
7985
8317
|
initializingSessions: new Set(),
|
|
7986
8318
|
sessionInitializations: new Map(),
|
|
8319
|
+
sessionImports: new Map(),
|
|
7987
8320
|
tombstones: new Map(snapshot.tombstones.map((entry) => [entry.sessionId, entry])),
|
|
7988
8321
|
tombstoneCleanups: new Map(),
|
|
7989
8322
|
activeRuns: new Map(),
|
|
@@ -8397,6 +8730,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8397
8730
|
scopeId?: string,
|
|
8398
8731
|
scope?: TScope,
|
|
8399
8732
|
): Promise<boolean> {
|
|
8733
|
+
if (stored.session.origin !== undefined) {
|
|
8734
|
+
this.assertImportedSessionState(stored);
|
|
8735
|
+
return false;
|
|
8736
|
+
}
|
|
8400
8737
|
const beforeProjection = JSON.stringify({
|
|
8401
8738
|
messages: stored.messages,
|
|
8402
8739
|
stagedTerminals: stored.stagedTerminals,
|
|
@@ -9054,7 +9391,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9054
9391
|
this.stores.jobs.deleteSession(storageKey, sessionId),
|
|
9055
9392
|
this.stores.projections.deleteSession(storageKey, sessionId),
|
|
9056
9393
|
this.stores.permissions.deleteSession(storageKey, sessionId),
|
|
9057
|
-
|
|
9394
|
+
// An imported session refuses every project-management write, so it has no state to fence.
|
|
9395
|
+
...(tombstone.imported ? [] : [projectManagementCleanup()]),
|
|
9058
9396
|
]);
|
|
9059
9397
|
const errors = results
|
|
9060
9398
|
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
@@ -9916,6 +10254,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9916
10254
|
return;
|
|
9917
10255
|
}
|
|
9918
10256
|
if (state.lifecycle !== 'fenced') state.lifecycle = 'retiring';
|
|
10257
|
+
for (const staged of [...state.sessionImports.values()]) {
|
|
10258
|
+
if (staged.phase === 'staging') this.releaseStagedSessionImport(staged);
|
|
10259
|
+
}
|
|
9919
10260
|
const errors: unknown[] = [];
|
|
9920
10261
|
const listingSettlement = this.abortSlashCommandListings(storageKey, reason);
|
|
9921
10262
|
if (listingSettlement) await listingSettlement;
|
|
@@ -10048,6 +10389,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10048
10389
|
state.pendingPermissions.clear();
|
|
10049
10390
|
state.initializingSessions.clear();
|
|
10050
10391
|
state.sessionInitializations.clear();
|
|
10392
|
+
state.sessionImports.clear();
|
|
10051
10393
|
}
|
|
10052
10394
|
|
|
10053
10395
|
private async disposeInternal(): Promise<void> {
|
|
@@ -10388,6 +10730,138 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10388
10730
|
return stored;
|
|
10389
10731
|
}
|
|
10390
10732
|
|
|
10733
|
+
/** Refuses an operation that would change an imported session's history or model context. */
|
|
10734
|
+
private assertSessionWritable(stored: IStoredSessionState): void {
|
|
10735
|
+
if (stored.session.origin !== undefined) {
|
|
10736
|
+
throw this.trustInternalError(new FlexHarnessImportedSessionError(stored.session.sessionId));
|
|
10737
|
+
}
|
|
10738
|
+
}
|
|
10739
|
+
|
|
10740
|
+
/** An imported session never ran here, so it has no hidden stage and no reversion state. */
|
|
10741
|
+
private assertImportedSessionState(stored: IStoredSessionState): void {
|
|
10742
|
+
if (
|
|
10743
|
+
stored.stagedTerminals.length > 0
|
|
10744
|
+
|| stored.reversionSegments.length > 0
|
|
10745
|
+
|| stored.excludedRunIds.length > 0
|
|
10746
|
+
|| stored.pendingReversion !== undefined
|
|
10747
|
+
|| stored.pendingReversionReleases.length > 0
|
|
10748
|
+
|| stored.messages.some((message) => message.status === 'streaming')
|
|
10749
|
+
) {
|
|
10750
|
+
throw new FlexHarnessValidationError(
|
|
10751
|
+
`Imported session "${stored.session.sessionId}" has run state it cannot have.`,
|
|
10752
|
+
);
|
|
10753
|
+
}
|
|
10754
|
+
}
|
|
10755
|
+
|
|
10756
|
+
private assertNoCommittingSessionImport(state: IStorageState, sessionId: string): void {
|
|
10757
|
+
if (state.sessionImports.get(sessionId)?.phase === 'committing') {
|
|
10758
|
+
throw new FlexHarnessSessionBusyError(sessionId, 'is being imported');
|
|
10759
|
+
}
|
|
10760
|
+
}
|
|
10761
|
+
|
|
10762
|
+
private findStagedSessionImport(
|
|
10763
|
+
handle: IFlexSessionImportHandle,
|
|
10764
|
+
): IStagedSessionImport | undefined {
|
|
10765
|
+
const validated = validateSessionImportHandle(handle);
|
|
10766
|
+
const staged = this.stagedSessionImports.get(validated.importId);
|
|
10767
|
+
if (
|
|
10768
|
+
staged
|
|
10769
|
+
&& (staged.scopeId !== validated.scopeId || staged.sessionId !== validated.sessionId)
|
|
10770
|
+
) {
|
|
10771
|
+
throw new FlexHarnessValidationError('The session import handle does not match its import.');
|
|
10772
|
+
}
|
|
10773
|
+
return staged;
|
|
10774
|
+
}
|
|
10775
|
+
|
|
10776
|
+
private requireStagedSessionImport(handle: IFlexSessionImportHandle): IStagedSessionImport {
|
|
10777
|
+
const staged = this.findStagedSessionImport(handle);
|
|
10778
|
+
if (!staged) throw new FlexHarnessNotFoundError('Session import', handle.importId);
|
|
10779
|
+
if (staged.phase !== 'staging') {
|
|
10780
|
+
throw new FlexHarnessSessionBusyError(staged.sessionId, 'is being committed');
|
|
10781
|
+
}
|
|
10782
|
+
return staged;
|
|
10783
|
+
}
|
|
10784
|
+
|
|
10785
|
+
private armStagedImportIdleTimer(staged: IStagedSessionImport): void {
|
|
10786
|
+
this.clearStagedImportIdleTimer(staged);
|
|
10787
|
+
const timer = setTimeout(() => {
|
|
10788
|
+
if (staged.idleTimer !== timer) return;
|
|
10789
|
+
delete staged.idleTimer;
|
|
10790
|
+
if (staged.phase === 'staging') this.releaseStagedSessionImport(staged);
|
|
10791
|
+
}, this.sessionImportLimits.stagedImportIdleTimeoutMs);
|
|
10792
|
+
timer.unref();
|
|
10793
|
+
staged.idleTimer = timer;
|
|
10794
|
+
}
|
|
10795
|
+
|
|
10796
|
+
private clearStagedImportIdleTimer(staged: IStagedSessionImport): void {
|
|
10797
|
+
if (staged.idleTimer === undefined) return;
|
|
10798
|
+
clearTimeout(staged.idleTimer);
|
|
10799
|
+
delete staged.idleTimer;
|
|
10800
|
+
}
|
|
10801
|
+
|
|
10802
|
+
private releaseStagedSessionImport(staged: IStagedSessionImport): void {
|
|
10803
|
+
this.clearStagedImportIdleTimer(staged);
|
|
10804
|
+
if (this.stagedSessionImports.get(staged.importId) === staged) {
|
|
10805
|
+
this.stagedSessionImports.delete(staged.importId);
|
|
10806
|
+
}
|
|
10807
|
+
if (staged.state.sessionImports.get(staged.sessionId) === staged) {
|
|
10808
|
+
staged.state.sessionImports.delete(staged.sessionId);
|
|
10809
|
+
staged.state.initializingSessions.delete(staged.sessionId);
|
|
10810
|
+
}
|
|
10811
|
+
staged.runs = [];
|
|
10812
|
+
staged.runIds.clear();
|
|
10813
|
+
staged.messageIds.clear();
|
|
10814
|
+
}
|
|
10815
|
+
|
|
10816
|
+
/**
|
|
10817
|
+
* An imported child session belongs to a committed imported parent whose run `parent.runId` holds
|
|
10818
|
+
* the tool call `parent.toolCallId` naming the child, and no other session claims that tool call.
|
|
10819
|
+
*/
|
|
10820
|
+
private requireImportParent(
|
|
10821
|
+
state: IStorageState,
|
|
10822
|
+
sessionId: string,
|
|
10823
|
+
parent: NonNullable<INormalizedSessionImportOptions['parent']>,
|
|
10824
|
+
): IStoredSessionState {
|
|
10825
|
+
const parentStored = state.initializingSessions.has(parent.sessionId)
|
|
10826
|
+
|| state.tombstones.has(parent.sessionId)
|
|
10827
|
+
? undefined
|
|
10828
|
+
: state.sessions.get(parent.sessionId);
|
|
10829
|
+
if (!parentStored?.session.origin) {
|
|
10830
|
+
throw this.trustInternalError(new FlexHarnessValidationError(
|
|
10831
|
+
`The parent of imported session "${sessionId}" must be a committed imported session.`,
|
|
10832
|
+
));
|
|
10833
|
+
}
|
|
10834
|
+
if ((parentStored.session.depth ?? 0) + 1 > FLEX_SESSION_IMPORT_MAX_DEPTH) {
|
|
10835
|
+
throw this.trustInternalError(new FlexHarnessValidationError(
|
|
10836
|
+
`Imported sessions nest at most ${FLEX_SESSION_IMPORT_MAX_DEPTH} levels deep.`,
|
|
10837
|
+
));
|
|
10838
|
+
}
|
|
10839
|
+
const referenced = parentStored.messages.some((message) =>
|
|
10840
|
+
message.role === 'assistant'
|
|
10841
|
+
&& message.runId === parent.runId
|
|
10842
|
+
&& message.parts.some((part) =>
|
|
10843
|
+
part.type === 'tool'
|
|
10844
|
+
&& part.toolCallId === parent.toolCallId
|
|
10845
|
+
&& part.childSessionId === sessionId));
|
|
10846
|
+
if (!referenced) {
|
|
10847
|
+
throw this.trustInternalError(new FlexHarnessValidationError(
|
|
10848
|
+
`Parent run "${parent.runId}" has no tool call "${parent.toolCallId}" whose childSessionId is "${sessionId}".`,
|
|
10849
|
+
));
|
|
10850
|
+
}
|
|
10851
|
+
for (const stored of state.sessions.values()) {
|
|
10852
|
+
if (
|
|
10853
|
+
stored.session.parentSessionId === parent.sessionId
|
|
10854
|
+
&& stored.session.parentRunId === parent.runId
|
|
10855
|
+
&& stored.session.parentToolCallId === parent.toolCallId
|
|
10856
|
+
) {
|
|
10857
|
+
throw this.trustInternalError(new FlexHarnessValidationError(
|
|
10858
|
+
`Session "${stored.session.sessionId}" already belongs to tool call "${parent.toolCallId}".`,
|
|
10859
|
+
));
|
|
10860
|
+
}
|
|
10861
|
+
}
|
|
10862
|
+
return parentStored;
|
|
10863
|
+
}
|
|
10864
|
+
|
|
10391
10865
|
private requireMessage(stored: IStoredSessionState, messageId: string): IFlexMessage {
|
|
10392
10866
|
const message = this.visibleMessages(stored).find((entry) => entry.messageId === messageId);
|
|
10393
10867
|
if (!message) throw new FlexHarnessNotFoundError('Message', messageId);
|