@modelprofile.com/flexharness 9.4.0 → 9.5.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 +404 -19
- package/dist_ts/errors.d.ts +5 -0
- package/dist_ts/errors.js +10 -1
- package/dist_ts/interfaces.d.ts +107 -0
- package/dist_ts/interfaces.js +19 -1
- package/dist_ts/utils.json.d.ts +5 -0
- package/dist_ts/utils.json.js +34 -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 +83 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +481 -15
- package/ts/errors.ts +11 -0
- package/ts/interfaces.ts +122 -0
- package/ts/readme.md +83 -0
- package/ts/utils.json.ts +37 -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,271 @@ 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
|
+
});
|
|
1232
|
+
});
|
|
1233
|
+
reserved = true;
|
|
1234
|
+
await this.stores.projections.save(state.storageKey, sessionId, {
|
|
1235
|
+
schemaVersion: 3,
|
|
1236
|
+
revision: 1,
|
|
1237
|
+
messages: staged.runs.flatMap((run) => [run.user, run.assistant]),
|
|
1238
|
+
stagedTerminals: [],
|
|
1239
|
+
reversionSegments: [],
|
|
1240
|
+
revertCursor: 0,
|
|
1241
|
+
excludedRunIds: [],
|
|
1242
|
+
pendingReversionReleases: [],
|
|
1243
|
+
}, 0);
|
|
1244
|
+
const runtime = await this.loadSessionRuntime(state, metadata, scopeId, scope);
|
|
1245
|
+
loaded = runtime;
|
|
1246
|
+
await this.mutateScope(state, () => {
|
|
1247
|
+
if (
|
|
1248
|
+
state.sessions.has(sessionId)
|
|
1249
|
+
|| state.tombstones.get(sessionId)?.sessionGenerationId !== metadata.sessionGenerationId
|
|
1250
|
+
) {
|
|
1251
|
+
throw this.trustInternalError(new FlexHarnessAbortError(
|
|
1252
|
+
'The session import lost its reservation before it was committed.',
|
|
1253
|
+
));
|
|
1254
|
+
}
|
|
1255
|
+
if (options.parent) this.requireImportParent(state, sessionId, options.parent);
|
|
1256
|
+
state.tombstones.delete(sessionId);
|
|
1257
|
+
state.sessions.set(sessionId, runtime);
|
|
1258
|
+
});
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
const projected = this.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-import');
|
|
1261
|
+
const errors: unknown[] = [projected];
|
|
1262
|
+
if (loaded && state.sessions.get(sessionId) !== loaded) {
|
|
1263
|
+
try {
|
|
1264
|
+
await this.closeStoredSession(loaded);
|
|
1265
|
+
} catch (closeError) {
|
|
1266
|
+
this.orphanedStoredSessions.add(loaded);
|
|
1267
|
+
errors.push(closeError);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
// A fenced namespace cannot tell whether its last save was persisted; its next load resolves
|
|
1271
|
+
// the reservation instead: a surviving tombstone is cleaned up, a published session is kept.
|
|
1272
|
+
if (reserved && state.tombstones.has(sessionId) && state.lifecycle !== 'fenced') {
|
|
1273
|
+
completeInitialization();
|
|
1274
|
+
try {
|
|
1275
|
+
await this.finishTombstoneCleanup(state, sessionId, scopeId, { scopeId, scope });
|
|
1276
|
+
} catch (cleanupError) {
|
|
1277
|
+
errors.push(this.projectOperationError(
|
|
1278
|
+
cleanupError,
|
|
1279
|
+
'persistence',
|
|
1280
|
+
scopeId,
|
|
1281
|
+
sessionId,
|
|
1282
|
+
'session-import-cleanup',
|
|
1283
|
+
));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
throw combineErrors(errors);
|
|
1287
|
+
} finally {
|
|
1288
|
+
this.releaseStagedSessionImport(staged);
|
|
1289
|
+
completeInitialization();
|
|
1290
|
+
}
|
|
1291
|
+
const result = publicSnapshot(metadata);
|
|
1292
|
+
this.emitEvent(scopeId, metadata, { type: 'session.created', session: result });
|
|
1293
|
+
return result;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/** Discards a staged import. Nothing of it was persisted; an unknown or finished handle is ignored. */
|
|
1297
|
+
public async abortSessionImport(handle: IFlexSessionImportHandle): Promise<void> {
|
|
1298
|
+
const staged = this.findStagedSessionImport(handle);
|
|
1299
|
+
if (!staged) return;
|
|
1300
|
+
if (staged.phase === 'committing') {
|
|
1301
|
+
throw new FlexHarnessSessionBusyError(staged.sessionId, 'is being committed');
|
|
1302
|
+
}
|
|
1303
|
+
this.releaseStagedSessionImport(staged);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
999
1306
|
public async getSession(scopeId: string, sessionId: string): Promise<IFlexSession> {
|
|
1000
1307
|
const { state } = await this.resolveState(scopeId);
|
|
1001
1308
|
await state.scopeQueue;
|
|
@@ -1068,7 +1375,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1068
1375
|
scopeId: string,
|
|
1069
1376
|
sessionId: string,
|
|
1070
1377
|
): Promise<IFlexProjectStateResult> {
|
|
1071
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1378
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
1072
1379
|
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1073
1380
|
return this.projectStateResult(snapshot);
|
|
1074
1381
|
}
|
|
@@ -1077,7 +1384,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1077
1384
|
scopeId: string,
|
|
1078
1385
|
sessionId: string,
|
|
1079
1386
|
): Promise<IFlexProjectTasksResult> {
|
|
1080
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1387
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
1081
1388
|
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1082
1389
|
return this.projectTasksResult(snapshot, snapshot.tasks);
|
|
1083
1390
|
}
|
|
@@ -1088,7 +1395,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1088
1395
|
input: IFlexCreateProjectTaskInput,
|
|
1089
1396
|
): Promise<IFlexProjectTaskResult> {
|
|
1090
1397
|
validateCreateProjectTaskInput(input);
|
|
1091
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1398
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1092
1399
|
return this.createProjectTaskInState(
|
|
1093
1400
|
state,
|
|
1094
1401
|
stored,
|
|
@@ -1103,7 +1410,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1103
1410
|
input: IFlexUpdateProjectTaskInput,
|
|
1104
1411
|
): Promise<IFlexProjectTaskResult> {
|
|
1105
1412
|
validateUpdateProjectTaskInput(input);
|
|
1106
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1413
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1107
1414
|
return this.updateProjectTaskInState(
|
|
1108
1415
|
state,
|
|
1109
1416
|
stored,
|
|
@@ -1118,7 +1425,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1118
1425
|
taskId: string,
|
|
1119
1426
|
): Promise<IFlexProjectTaskResult> {
|
|
1120
1427
|
validateProjectTaskId(taskId);
|
|
1121
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1428
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1122
1429
|
return this.deleteProjectTaskInState(
|
|
1123
1430
|
state,
|
|
1124
1431
|
stored,
|
|
@@ -1131,7 +1438,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1131
1438
|
scopeId: string,
|
|
1132
1439
|
sessionId: string,
|
|
1133
1440
|
): Promise<IFlexProjectTasksResult> {
|
|
1134
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1441
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1135
1442
|
return this.clearProjectTasksInState(
|
|
1136
1443
|
state,
|
|
1137
1444
|
stored,
|
|
@@ -1143,7 +1450,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1143
1450
|
scopeId: string,
|
|
1144
1451
|
sessionId: string,
|
|
1145
1452
|
): Promise<IFlexProjectGoalResult> {
|
|
1146
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1453
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
1147
1454
|
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1148
1455
|
return this.projectGoalResult(snapshot);
|
|
1149
1456
|
}
|
|
@@ -1159,7 +1466,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1159
1466
|
FLEX_PROJECT_MANAGEMENT_LIMITS.maxGoalBytes,
|
|
1160
1467
|
true,
|
|
1161
1468
|
);
|
|
1162
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1469
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1163
1470
|
return this.setProjectGoalInState(
|
|
1164
1471
|
state,
|
|
1165
1472
|
stored,
|
|
@@ -1172,7 +1479,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1172
1479
|
scopeId: string,
|
|
1173
1480
|
sessionId: string,
|
|
1174
1481
|
): Promise<IFlexProjectGoalResult> {
|
|
1175
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1482
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1176
1483
|
return this.clearProjectGoalInState(
|
|
1177
1484
|
state,
|
|
1178
1485
|
stored,
|
|
@@ -1184,7 +1491,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1184
1491
|
scopeId: string,
|
|
1185
1492
|
sessionId: string,
|
|
1186
1493
|
): Promise<IFlexProjectScratchpadResult> {
|
|
1187
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1494
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
1188
1495
|
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1189
1496
|
return this.projectScratchpadResult(snapshot);
|
|
1190
1497
|
}
|
|
@@ -1199,7 +1506,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1199
1506
|
'project scratchpad',
|
|
1200
1507
|
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
1201
1508
|
);
|
|
1202
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1509
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1203
1510
|
return this.setProjectScratchpadInState(
|
|
1204
1511
|
state,
|
|
1205
1512
|
stored,
|
|
@@ -1218,7 +1525,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1218
1525
|
'project scratchpad append',
|
|
1219
1526
|
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
1220
1527
|
);
|
|
1221
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1528
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1222
1529
|
return this.appendProjectScratchpadInState(
|
|
1223
1530
|
state,
|
|
1224
1531
|
stored,
|
|
@@ -1231,7 +1538,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1231
1538
|
scopeId: string,
|
|
1232
1539
|
sessionId: string,
|
|
1233
1540
|
): Promise<IFlexProjectScratchpadResult> {
|
|
1234
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1541
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1235
1542
|
return this.clearProjectScratchpadInState(
|
|
1236
1543
|
state,
|
|
1237
1544
|
stored,
|
|
@@ -1295,6 +1602,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1295
1602
|
if (!tombstone || !matchesSessionGeneration(tombstone, validated.root)) {
|
|
1296
1603
|
return { matched: false };
|
|
1297
1604
|
}
|
|
1605
|
+
this.assertNoCommittingSessionImport(state, validated.root.sessionId);
|
|
1298
1606
|
const rootSessionId = tombstone.rootSessionId ?? tombstone.sessionId;
|
|
1299
1607
|
const descendantRoots = this.descendantTombstoneRoots(state, rootSessionId);
|
|
1300
1608
|
this.assertAuthorizedSessionGenerationCohort(
|
|
@@ -1323,6 +1631,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1323
1631
|
this.assertNoAmbientSlashCommandTeardown({ storageKey: state.storageKey });
|
|
1324
1632
|
validateIdentifier(sessionId, 'sessionId');
|
|
1325
1633
|
await state.scopeQueue;
|
|
1634
|
+
this.assertNoCommittingSessionImport(state, sessionId);
|
|
1326
1635
|
return this.startSessionDeletion(state, scopeId, scope.scope, sessionId);
|
|
1327
1636
|
}
|
|
1328
1637
|
|
|
@@ -1644,12 +1953,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1644
1953
|
signal,
|
|
1645
1954
|
);
|
|
1646
1955
|
signal.throwIfAborted();
|
|
1647
|
-
|
|
1956
|
+
const descriptors = this.createSlashCommandDescriptors(
|
|
1648
1957
|
state,
|
|
1649
1958
|
stored,
|
|
1650
1959
|
sessionAvailable,
|
|
1651
1960
|
reversionIdleReason,
|
|
1652
1961
|
);
|
|
1962
|
+
if (stored.session.origin === undefined) return descriptors;
|
|
1963
|
+
return publicSnapshot(descriptors.map((descriptor): IFlexSlashCommandDescriptor => ({
|
|
1964
|
+
...descriptor,
|
|
1965
|
+
available: false,
|
|
1966
|
+
unavailableReason: importedSessionUnavailableReason,
|
|
1967
|
+
})));
|
|
1653
1968
|
});
|
|
1654
1969
|
}
|
|
1655
1970
|
|
|
@@ -1788,6 +2103,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1788
2103
|
this.assertOpen();
|
|
1789
2104
|
this.assertStateAcceptingWork(state);
|
|
1790
2105
|
const stored = this.requireSession(state, sessionId);
|
|
2106
|
+
this.assertSessionWritable(stored);
|
|
1791
2107
|
const sessionKey = this.slashCommandSessionKey(state.storageKey, sessionId);
|
|
1792
2108
|
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
1793
2109
|
if (invocationOwner?.sessionKey === sessionKey) {
|
|
@@ -2383,6 +2699,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2383
2699
|
}
|
|
2384
2700
|
const { scope, state } = await this.resolveState(scopeId);
|
|
2385
2701
|
const stored = this.requireSession(state, sessionId);
|
|
2702
|
+
this.assertSessionWritable(stored);
|
|
2386
2703
|
this.assertStateAcceptingWork(state);
|
|
2387
2704
|
let canonical: plugins.TAgentToolExecutionReconciliationOptions;
|
|
2388
2705
|
if (reconciliation.resolution === 'executed') {
|
|
@@ -2422,6 +2739,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2422
2739
|
this.validateRuntimeEvent(event);
|
|
2423
2740
|
const { scope, state } = await this.resolveState(scopeId);
|
|
2424
2741
|
const stored = this.requireSession(state, sessionId);
|
|
2742
|
+
this.assertSessionWritable(stored);
|
|
2425
2743
|
this.assertStateAcceptingWork(state);
|
|
2426
2744
|
try {
|
|
2427
2745
|
await this.withCompactorContext(
|
|
@@ -2441,6 +2759,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2441
2759
|
const { modelHint } = validateCompactSessionOptions(options);
|
|
2442
2760
|
const { scope, state } = await this.resolveState(scopeId);
|
|
2443
2761
|
const stored = this.requireSession(state, sessionId);
|
|
2762
|
+
this.assertSessionWritable(stored);
|
|
2444
2763
|
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
2445
2764
|
if (invocationOwner?.sessionKey === this.slashCommandSessionKey(state.storageKey, sessionId)) {
|
|
2446
2765
|
throw this.trustInternalError(new FlexHarnessSlashCommandReentryError(sessionId));
|
|
@@ -2688,6 +3007,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2688
3007
|
signal?: AbortSignal,
|
|
2689
3008
|
): Promise<string> {
|
|
2690
3009
|
const stored = this.requireSession(state, sessionId);
|
|
3010
|
+
this.assertSessionWritable(stored);
|
|
2691
3011
|
this.assertStateAcceptingWork(state);
|
|
2692
3012
|
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
2693
3013
|
if (invocationOwner?.sessionKey === this.slashCommandSessionKey(state.storageKey, sessionId)) {
|
|
@@ -3040,6 +3360,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3040
3360
|
}
|
|
3041
3361
|
const { scope, state } = await this.resolveState(scopeId);
|
|
3042
3362
|
const stored = this.requireSession(state, sessionId);
|
|
3363
|
+
this.assertSessionWritable(stored);
|
|
3043
3364
|
this.assertStateAcceptingWork(state);
|
|
3044
3365
|
this.requireSessionAvailableForSlashCommand(state, stored, 'archive');
|
|
3045
3366
|
const compaction = compactionEventId === undefined
|
|
@@ -3239,6 +3560,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3239
3560
|
}
|
|
3240
3561
|
this.assertStateAcceptingWork(state);
|
|
3241
3562
|
const stored = this.requireSession(state, sessionId);
|
|
3563
|
+
this.assertSessionWritable(stored);
|
|
3242
3564
|
const sessionKey = this.slashCommandSessionKey(state.storageKey, sessionId);
|
|
3243
3565
|
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
3244
3566
|
if (invocationOwner?.sessionKey === sessionKey) {
|
|
@@ -4209,13 +4531,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4209
4531
|
private async resolveProjectManagementSession(
|
|
4210
4532
|
scopeId: string,
|
|
4211
4533
|
sessionId: string,
|
|
4534
|
+
access: 'read' | 'write',
|
|
4212
4535
|
): Promise<{ state: IStorageState; stored: IStoredSessionState }> {
|
|
4213
4536
|
this.requireProjectManagementTools();
|
|
4214
4537
|
const { state } = await this.resolveState(scopeId);
|
|
4215
4538
|
await state.scopeQueue;
|
|
4216
4539
|
this.assertOpen();
|
|
4217
4540
|
this.assertStateAcceptingWork(state);
|
|
4218
|
-
|
|
4541
|
+
const stored = this.requireSession(state, sessionId);
|
|
4542
|
+
if (access === 'write') this.assertSessionWritable(stored);
|
|
4543
|
+
return { state, stored };
|
|
4219
4544
|
}
|
|
4220
4545
|
|
|
4221
4546
|
private assertProjectManagementSession(
|
|
@@ -7984,6 +8309,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7984
8309
|
sessionDeletions: new Map(),
|
|
7985
8310
|
initializingSessions: new Set(),
|
|
7986
8311
|
sessionInitializations: new Map(),
|
|
8312
|
+
sessionImports: new Map(),
|
|
7987
8313
|
tombstones: new Map(snapshot.tombstones.map((entry) => [entry.sessionId, entry])),
|
|
7988
8314
|
tombstoneCleanups: new Map(),
|
|
7989
8315
|
activeRuns: new Map(),
|
|
@@ -8397,6 +8723,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8397
8723
|
scopeId?: string,
|
|
8398
8724
|
scope?: TScope,
|
|
8399
8725
|
): Promise<boolean> {
|
|
8726
|
+
if (stored.session.origin !== undefined) {
|
|
8727
|
+
this.assertImportedSessionState(stored);
|
|
8728
|
+
return false;
|
|
8729
|
+
}
|
|
8400
8730
|
const beforeProjection = JSON.stringify({
|
|
8401
8731
|
messages: stored.messages,
|
|
8402
8732
|
stagedTerminals: stored.stagedTerminals,
|
|
@@ -9916,6 +10246,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9916
10246
|
return;
|
|
9917
10247
|
}
|
|
9918
10248
|
if (state.lifecycle !== 'fenced') state.lifecycle = 'retiring';
|
|
10249
|
+
for (const staged of [...state.sessionImports.values()]) {
|
|
10250
|
+
if (staged.phase === 'staging') this.releaseStagedSessionImport(staged);
|
|
10251
|
+
}
|
|
9919
10252
|
const errors: unknown[] = [];
|
|
9920
10253
|
const listingSettlement = this.abortSlashCommandListings(storageKey, reason);
|
|
9921
10254
|
if (listingSettlement) await listingSettlement;
|
|
@@ -10048,6 +10381,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10048
10381
|
state.pendingPermissions.clear();
|
|
10049
10382
|
state.initializingSessions.clear();
|
|
10050
10383
|
state.sessionInitializations.clear();
|
|
10384
|
+
state.sessionImports.clear();
|
|
10051
10385
|
}
|
|
10052
10386
|
|
|
10053
10387
|
private async disposeInternal(): Promise<void> {
|
|
@@ -10388,6 +10722,138 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10388
10722
|
return stored;
|
|
10389
10723
|
}
|
|
10390
10724
|
|
|
10725
|
+
/** Refuses an operation that would change an imported session's history or model context. */
|
|
10726
|
+
private assertSessionWritable(stored: IStoredSessionState): void {
|
|
10727
|
+
if (stored.session.origin !== undefined) {
|
|
10728
|
+
throw this.trustInternalError(new FlexHarnessImportedSessionError(stored.session.sessionId));
|
|
10729
|
+
}
|
|
10730
|
+
}
|
|
10731
|
+
|
|
10732
|
+
/** An imported session never ran here, so it has no hidden stage and no reversion state. */
|
|
10733
|
+
private assertImportedSessionState(stored: IStoredSessionState): void {
|
|
10734
|
+
if (
|
|
10735
|
+
stored.stagedTerminals.length > 0
|
|
10736
|
+
|| stored.reversionSegments.length > 0
|
|
10737
|
+
|| stored.excludedRunIds.length > 0
|
|
10738
|
+
|| stored.pendingReversion !== undefined
|
|
10739
|
+
|| stored.pendingReversionReleases.length > 0
|
|
10740
|
+
|| stored.messages.some((message) => message.status === 'streaming')
|
|
10741
|
+
) {
|
|
10742
|
+
throw new FlexHarnessValidationError(
|
|
10743
|
+
`Imported session "${stored.session.sessionId}" has run state it cannot have.`,
|
|
10744
|
+
);
|
|
10745
|
+
}
|
|
10746
|
+
}
|
|
10747
|
+
|
|
10748
|
+
private assertNoCommittingSessionImport(state: IStorageState, sessionId: string): void {
|
|
10749
|
+
if (state.sessionImports.get(sessionId)?.phase === 'committing') {
|
|
10750
|
+
throw new FlexHarnessSessionBusyError(sessionId, 'is being imported');
|
|
10751
|
+
}
|
|
10752
|
+
}
|
|
10753
|
+
|
|
10754
|
+
private findStagedSessionImport(
|
|
10755
|
+
handle: IFlexSessionImportHandle,
|
|
10756
|
+
): IStagedSessionImport | undefined {
|
|
10757
|
+
const validated = validateSessionImportHandle(handle);
|
|
10758
|
+
const staged = this.stagedSessionImports.get(validated.importId);
|
|
10759
|
+
if (
|
|
10760
|
+
staged
|
|
10761
|
+
&& (staged.scopeId !== validated.scopeId || staged.sessionId !== validated.sessionId)
|
|
10762
|
+
) {
|
|
10763
|
+
throw new FlexHarnessValidationError('The session import handle does not match its import.');
|
|
10764
|
+
}
|
|
10765
|
+
return staged;
|
|
10766
|
+
}
|
|
10767
|
+
|
|
10768
|
+
private requireStagedSessionImport(handle: IFlexSessionImportHandle): IStagedSessionImport {
|
|
10769
|
+
const staged = this.findStagedSessionImport(handle);
|
|
10770
|
+
if (!staged) throw new FlexHarnessNotFoundError('Session import', handle.importId);
|
|
10771
|
+
if (staged.phase !== 'staging') {
|
|
10772
|
+
throw new FlexHarnessSessionBusyError(staged.sessionId, 'is being committed');
|
|
10773
|
+
}
|
|
10774
|
+
return staged;
|
|
10775
|
+
}
|
|
10776
|
+
|
|
10777
|
+
private armStagedImportIdleTimer(staged: IStagedSessionImport): void {
|
|
10778
|
+
this.clearStagedImportIdleTimer(staged);
|
|
10779
|
+
const timer = setTimeout(() => {
|
|
10780
|
+
if (staged.idleTimer !== timer) return;
|
|
10781
|
+
delete staged.idleTimer;
|
|
10782
|
+
if (staged.phase === 'staging') this.releaseStagedSessionImport(staged);
|
|
10783
|
+
}, this.sessionImportLimits.stagedImportIdleTimeoutMs);
|
|
10784
|
+
timer.unref();
|
|
10785
|
+
staged.idleTimer = timer;
|
|
10786
|
+
}
|
|
10787
|
+
|
|
10788
|
+
private clearStagedImportIdleTimer(staged: IStagedSessionImport): void {
|
|
10789
|
+
if (staged.idleTimer === undefined) return;
|
|
10790
|
+
clearTimeout(staged.idleTimer);
|
|
10791
|
+
delete staged.idleTimer;
|
|
10792
|
+
}
|
|
10793
|
+
|
|
10794
|
+
private releaseStagedSessionImport(staged: IStagedSessionImport): void {
|
|
10795
|
+
this.clearStagedImportIdleTimer(staged);
|
|
10796
|
+
if (this.stagedSessionImports.get(staged.importId) === staged) {
|
|
10797
|
+
this.stagedSessionImports.delete(staged.importId);
|
|
10798
|
+
}
|
|
10799
|
+
if (staged.state.sessionImports.get(staged.sessionId) === staged) {
|
|
10800
|
+
staged.state.sessionImports.delete(staged.sessionId);
|
|
10801
|
+
staged.state.initializingSessions.delete(staged.sessionId);
|
|
10802
|
+
}
|
|
10803
|
+
staged.runs = [];
|
|
10804
|
+
staged.runIds.clear();
|
|
10805
|
+
staged.messageIds.clear();
|
|
10806
|
+
}
|
|
10807
|
+
|
|
10808
|
+
/**
|
|
10809
|
+
* An imported child session belongs to a committed imported parent whose run `parent.runId` holds
|
|
10810
|
+
* the tool call `parent.toolCallId` naming the child, and no other session claims that tool call.
|
|
10811
|
+
*/
|
|
10812
|
+
private requireImportParent(
|
|
10813
|
+
state: IStorageState,
|
|
10814
|
+
sessionId: string,
|
|
10815
|
+
parent: NonNullable<INormalizedSessionImportOptions['parent']>,
|
|
10816
|
+
): IStoredSessionState {
|
|
10817
|
+
const parentStored = state.initializingSessions.has(parent.sessionId)
|
|
10818
|
+
|| state.tombstones.has(parent.sessionId)
|
|
10819
|
+
? undefined
|
|
10820
|
+
: state.sessions.get(parent.sessionId);
|
|
10821
|
+
if (!parentStored?.session.origin) {
|
|
10822
|
+
throw this.trustInternalError(new FlexHarnessValidationError(
|
|
10823
|
+
`The parent of imported session "${sessionId}" must be a committed imported session.`,
|
|
10824
|
+
));
|
|
10825
|
+
}
|
|
10826
|
+
if ((parentStored.session.depth ?? 0) + 1 > FLEX_SESSION_IMPORT_MAX_DEPTH) {
|
|
10827
|
+
throw this.trustInternalError(new FlexHarnessValidationError(
|
|
10828
|
+
`Imported sessions nest at most ${FLEX_SESSION_IMPORT_MAX_DEPTH} levels deep.`,
|
|
10829
|
+
));
|
|
10830
|
+
}
|
|
10831
|
+
const referenced = parentStored.messages.some((message) =>
|
|
10832
|
+
message.role === 'assistant'
|
|
10833
|
+
&& message.runId === parent.runId
|
|
10834
|
+
&& message.parts.some((part) =>
|
|
10835
|
+
part.type === 'tool'
|
|
10836
|
+
&& part.toolCallId === parent.toolCallId
|
|
10837
|
+
&& part.childSessionId === sessionId));
|
|
10838
|
+
if (!referenced) {
|
|
10839
|
+
throw this.trustInternalError(new FlexHarnessValidationError(
|
|
10840
|
+
`Parent run "${parent.runId}" has no tool call "${parent.toolCallId}" whose childSessionId is "${sessionId}".`,
|
|
10841
|
+
));
|
|
10842
|
+
}
|
|
10843
|
+
for (const stored of state.sessions.values()) {
|
|
10844
|
+
if (
|
|
10845
|
+
stored.session.parentSessionId === parent.sessionId
|
|
10846
|
+
&& stored.session.parentRunId === parent.runId
|
|
10847
|
+
&& stored.session.parentToolCallId === parent.toolCallId
|
|
10848
|
+
) {
|
|
10849
|
+
throw this.trustInternalError(new FlexHarnessValidationError(
|
|
10850
|
+
`Session "${stored.session.sessionId}" already belongs to tool call "${parent.toolCallId}".`,
|
|
10851
|
+
));
|
|
10852
|
+
}
|
|
10853
|
+
}
|
|
10854
|
+
return parentStored;
|
|
10855
|
+
}
|
|
10856
|
+
|
|
10391
10857
|
private requireMessage(stored: IStoredSessionState, messageId: string): IFlexMessage {
|
|
10392
10858
|
const message = this.visibleMessages(stored).find((entry) => entry.messageId === messageId);
|
|
10393
10859
|
if (!message) throw new FlexHarnessNotFoundError('Message', messageId);
|