@modelprofile.com/flexharness 9.3.1 → 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 +39 -2
- package/dist_ts/classes.flexharness.js +451 -30
- package/dist_ts/errors.d.ts +5 -0
- package/dist_ts/errors.js +10 -1
- package/dist_ts/interfaces.d.ts +128 -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/dist_ts/utils.validation.d.ts +2 -1
- package/dist_ts/utils.validation.js +14 -1
- package/package.json +3 -3
- package/readme.md +107 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +557 -25
- package/ts/errors.ts +11 -0
- package/ts/interfaces.ts +146 -1
- package/ts/readme.md +107 -2
- package/ts/utils.json.ts +37 -0
- package/ts/utils.sessionimport.ts +327 -0
- package/ts/utils.validation.ts +16 -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,13 +26,17 @@ 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 {
|
|
32
|
+
IFlexAgentContextCompactionOptions,
|
|
30
33
|
IFlexAgentContextInvocation,
|
|
31
34
|
IFlexAgentSessionPolicy,
|
|
32
35
|
IFlexAffectedWorkspace,
|
|
33
36
|
IFlexBackgroundExecution,
|
|
37
|
+
IFlexBeginSessionImportOptions,
|
|
34
38
|
IFlexCallbackLimits,
|
|
39
|
+
IFlexCompactSessionOptions,
|
|
35
40
|
IFlexCreateProjectTaskInput,
|
|
36
41
|
IFlexCreateSessionOptions,
|
|
37
42
|
IFlexDeleteSessionGenerationCohortInput,
|
|
@@ -43,6 +48,7 @@ import type {
|
|
|
43
48
|
IFlexExecutionContextHandle,
|
|
44
49
|
IFlexHarnessOptions,
|
|
45
50
|
IFlexHarnessStores,
|
|
51
|
+
IFlexImportedRun,
|
|
46
52
|
IFlexMessage,
|
|
47
53
|
IFlexMessagePage,
|
|
48
54
|
IFlexMessagePageOptions,
|
|
@@ -83,6 +89,10 @@ import type {
|
|
|
83
89
|
IFlexSessionActivity,
|
|
84
90
|
IFlexSessionGeneration,
|
|
85
91
|
IFlexSessionGenerationCohortEntry,
|
|
92
|
+
IFlexSessionImportHandle,
|
|
93
|
+
IFlexSessionImportLimits,
|
|
94
|
+
IFlexSessionImportProgress,
|
|
95
|
+
IFlexSessionOrigin,
|
|
86
96
|
IFlexSessionReversionGroup,
|
|
87
97
|
IFlexSessionReversionInfo,
|
|
88
98
|
IFlexSessionTombstone,
|
|
@@ -104,6 +114,7 @@ import type {
|
|
|
104
114
|
TFlexAgentToolCallUpdateEvent,
|
|
105
115
|
TFlexAgentToolCallFinishEvent,
|
|
106
116
|
TFlexAgentToolSet,
|
|
117
|
+
TFlexBeginSessionImportResult,
|
|
107
118
|
TFlexExternalErrorProjector,
|
|
108
119
|
TFlexExternalErrorSource,
|
|
109
120
|
TFlexHarnessEvent,
|
|
@@ -182,8 +193,17 @@ import {
|
|
|
182
193
|
truncateUtf8,
|
|
183
194
|
type IRunResultProjection,
|
|
184
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';
|
|
185
204
|
import { wrapToolSet } from './utils.tooloutput.js';
|
|
186
205
|
import {
|
|
206
|
+
validateCompactSessionOptions,
|
|
187
207
|
validateCreateProjectTaskInput,
|
|
188
208
|
validateDeleteSessionGenerationCohortInput,
|
|
189
209
|
validateIdentifier,
|
|
@@ -206,6 +226,11 @@ type TRunPhase =
|
|
|
206
226
|
| 'finalizing'
|
|
207
227
|
| 'promoting';
|
|
208
228
|
|
|
229
|
+
/** The invocation a compaction runs under; a manual compaction also carries its requested model hint. */
|
|
230
|
+
interface ICompactorInvocation<TScope> extends IFlexAgentContextInvocation<TScope> {
|
|
231
|
+
readonly modelHint?: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
209
234
|
interface IStoredSessionState {
|
|
210
235
|
storageKey: string;
|
|
211
236
|
compactorContext?: IFlexAgentContextInvocation<unknown>;
|
|
@@ -291,6 +316,25 @@ interface IOrphanedExecutionContextOwner {
|
|
|
291
316
|
sessionId: string;
|
|
292
317
|
}
|
|
293
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
|
+
|
|
294
338
|
interface IStorageState {
|
|
295
339
|
storageKey: string;
|
|
296
340
|
compactorLifecycleController: AbortController;
|
|
@@ -302,6 +346,8 @@ interface IStorageState {
|
|
|
302
346
|
sessionDeletions: Map<string, Promise<void>>;
|
|
303
347
|
initializingSessions: Set<string>;
|
|
304
348
|
sessionInitializations: Map<string, Promise<void>>;
|
|
349
|
+
/** Staged session imports by session id; their ids are also in `initializingSessions`. */
|
|
350
|
+
sessionImports: Map<string, IStagedSessionImport>;
|
|
305
351
|
tombstones: Map<string, IFlexSessionTombstone>;
|
|
306
352
|
tombstoneCleanups: Map<string, Promise<void>>;
|
|
307
353
|
activeRuns: Map<string, IActiveRun>;
|
|
@@ -567,6 +613,7 @@ const resourceToolStemLength = 16;
|
|
|
567
613
|
const repairCancellationMessage = 'The process stopped before this run completed.';
|
|
568
614
|
const scopeRetirementMessage = 'The scope is being retired.';
|
|
569
615
|
const slashCommandSessionUnavailableReason = 'Session must be idle with no queued prompts.';
|
|
616
|
+
const importedSessionUnavailableReason = 'Session is an imported conversation and is read-only.';
|
|
570
617
|
const externalErrorFallback: IFlexErrorInfo = Object.freeze({
|
|
571
618
|
name: 'FlexHarnessExternalError',
|
|
572
619
|
message: 'The model operation failed.',
|
|
@@ -706,6 +753,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
706
753
|
private readonly callbackLimits: Required<IFlexCallbackLimits>;
|
|
707
754
|
private readonly promptQueueLimits: Required<IFlexPromptQueueLimits>;
|
|
708
755
|
private readonly reversionLimits: Required<IFlexReversionLimits>;
|
|
756
|
+
private readonly sessionImportLimits: Required<IFlexSessionImportLimits>;
|
|
709
757
|
private readonly reversionMaintenanceTimeoutMs: number;
|
|
710
758
|
private readonly turnReversionProvider: IFlexHarnessOptions<TScope>['turnReversionProvider'];
|
|
711
759
|
private readonly reversionPolicy: TFlexReversionPolicy;
|
|
@@ -735,8 +783,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
735
783
|
private readonly projectManagementQueues = new Map<string, Promise<void>>();
|
|
736
784
|
private readonly activeSlashCommandExecutions = new Map<string, IActiveSlashCommandExecution>();
|
|
737
785
|
private readonly activeSlashCommandListings = new Set<IActiveSlashCommandListing>();
|
|
786
|
+
private readonly stagedSessionImports = new Map<string, IStagedSessionImport>();
|
|
738
787
|
private readonly compactorInvocationContext = new plugins.AsyncLocalStorage<
|
|
739
|
-
|
|
788
|
+
ICompactorInvocation<TScope>
|
|
740
789
|
>();
|
|
741
790
|
private readonly slashCommandInvocationContext = new plugins.AsyncLocalStorage<
|
|
742
791
|
ISlashCommandInvocationOwner
|
|
@@ -771,6 +820,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
771
820
|
this.callbackLimits = resolveCallbackLimits(options.callbackLimits);
|
|
772
821
|
this.promptQueueLimits = resolvePromptQueueLimits(options.promptQueueLimits);
|
|
773
822
|
this.reversionLimits = resolveReversionLimits(options.reversionLimits);
|
|
823
|
+
this.sessionImportLimits = resolveSessionImportLimits(options.sessionImportLimits);
|
|
774
824
|
this.reversionMaintenanceTimeoutMs = resolveReversionMaintenanceTimeoutMs(options);
|
|
775
825
|
this.turnReversionProvider = options.turnReversionProvider;
|
|
776
826
|
this.reversionPolicy = options.reversionPolicy ?? 'transcript-optional';
|
|
@@ -988,6 +1038,271 @@ export class FlexHarness<TScope = unknown> {
|
|
|
988
1038
|
return result;
|
|
989
1039
|
}
|
|
990
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
|
+
|
|
991
1306
|
public async getSession(scopeId: string, sessionId: string): Promise<IFlexSession> {
|
|
992
1307
|
const { state } = await this.resolveState(scopeId);
|
|
993
1308
|
await state.scopeQueue;
|
|
@@ -1060,7 +1375,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1060
1375
|
scopeId: string,
|
|
1061
1376
|
sessionId: string,
|
|
1062
1377
|
): Promise<IFlexProjectStateResult> {
|
|
1063
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1378
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
1064
1379
|
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1065
1380
|
return this.projectStateResult(snapshot);
|
|
1066
1381
|
}
|
|
@@ -1069,7 +1384,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1069
1384
|
scopeId: string,
|
|
1070
1385
|
sessionId: string,
|
|
1071
1386
|
): Promise<IFlexProjectTasksResult> {
|
|
1072
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1387
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
1073
1388
|
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1074
1389
|
return this.projectTasksResult(snapshot, snapshot.tasks);
|
|
1075
1390
|
}
|
|
@@ -1080,7 +1395,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1080
1395
|
input: IFlexCreateProjectTaskInput,
|
|
1081
1396
|
): Promise<IFlexProjectTaskResult> {
|
|
1082
1397
|
validateCreateProjectTaskInput(input);
|
|
1083
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1398
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1084
1399
|
return this.createProjectTaskInState(
|
|
1085
1400
|
state,
|
|
1086
1401
|
stored,
|
|
@@ -1095,7 +1410,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1095
1410
|
input: IFlexUpdateProjectTaskInput,
|
|
1096
1411
|
): Promise<IFlexProjectTaskResult> {
|
|
1097
1412
|
validateUpdateProjectTaskInput(input);
|
|
1098
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1413
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1099
1414
|
return this.updateProjectTaskInState(
|
|
1100
1415
|
state,
|
|
1101
1416
|
stored,
|
|
@@ -1110,7 +1425,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1110
1425
|
taskId: string,
|
|
1111
1426
|
): Promise<IFlexProjectTaskResult> {
|
|
1112
1427
|
validateProjectTaskId(taskId);
|
|
1113
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1428
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1114
1429
|
return this.deleteProjectTaskInState(
|
|
1115
1430
|
state,
|
|
1116
1431
|
stored,
|
|
@@ -1123,7 +1438,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1123
1438
|
scopeId: string,
|
|
1124
1439
|
sessionId: string,
|
|
1125
1440
|
): Promise<IFlexProjectTasksResult> {
|
|
1126
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1441
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1127
1442
|
return this.clearProjectTasksInState(
|
|
1128
1443
|
state,
|
|
1129
1444
|
stored,
|
|
@@ -1135,7 +1450,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1135
1450
|
scopeId: string,
|
|
1136
1451
|
sessionId: string,
|
|
1137
1452
|
): Promise<IFlexProjectGoalResult> {
|
|
1138
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1453
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
1139
1454
|
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1140
1455
|
return this.projectGoalResult(snapshot);
|
|
1141
1456
|
}
|
|
@@ -1151,7 +1466,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1151
1466
|
FLEX_PROJECT_MANAGEMENT_LIMITS.maxGoalBytes,
|
|
1152
1467
|
true,
|
|
1153
1468
|
);
|
|
1154
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1469
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1155
1470
|
return this.setProjectGoalInState(
|
|
1156
1471
|
state,
|
|
1157
1472
|
stored,
|
|
@@ -1164,7 +1479,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1164
1479
|
scopeId: string,
|
|
1165
1480
|
sessionId: string,
|
|
1166
1481
|
): Promise<IFlexProjectGoalResult> {
|
|
1167
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1482
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1168
1483
|
return this.clearProjectGoalInState(
|
|
1169
1484
|
state,
|
|
1170
1485
|
stored,
|
|
@@ -1176,7 +1491,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1176
1491
|
scopeId: string,
|
|
1177
1492
|
sessionId: string,
|
|
1178
1493
|
): Promise<IFlexProjectScratchpadResult> {
|
|
1179
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1494
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
1180
1495
|
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1181
1496
|
return this.projectScratchpadResult(snapshot);
|
|
1182
1497
|
}
|
|
@@ -1191,7 +1506,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1191
1506
|
'project scratchpad',
|
|
1192
1507
|
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
1193
1508
|
);
|
|
1194
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1509
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1195
1510
|
return this.setProjectScratchpadInState(
|
|
1196
1511
|
state,
|
|
1197
1512
|
stored,
|
|
@@ -1210,7 +1525,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1210
1525
|
'project scratchpad append',
|
|
1211
1526
|
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
1212
1527
|
);
|
|
1213
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1528
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1214
1529
|
return this.appendProjectScratchpadInState(
|
|
1215
1530
|
state,
|
|
1216
1531
|
stored,
|
|
@@ -1223,7 +1538,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1223
1538
|
scopeId: string,
|
|
1224
1539
|
sessionId: string,
|
|
1225
1540
|
): Promise<IFlexProjectScratchpadResult> {
|
|
1226
|
-
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1541
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
1227
1542
|
return this.clearProjectScratchpadInState(
|
|
1228
1543
|
state,
|
|
1229
1544
|
stored,
|
|
@@ -1287,6 +1602,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1287
1602
|
if (!tombstone || !matchesSessionGeneration(tombstone, validated.root)) {
|
|
1288
1603
|
return { matched: false };
|
|
1289
1604
|
}
|
|
1605
|
+
this.assertNoCommittingSessionImport(state, validated.root.sessionId);
|
|
1290
1606
|
const rootSessionId = tombstone.rootSessionId ?? tombstone.sessionId;
|
|
1291
1607
|
const descendantRoots = this.descendantTombstoneRoots(state, rootSessionId);
|
|
1292
1608
|
this.assertAuthorizedSessionGenerationCohort(
|
|
@@ -1315,6 +1631,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1315
1631
|
this.assertNoAmbientSlashCommandTeardown({ storageKey: state.storageKey });
|
|
1316
1632
|
validateIdentifier(sessionId, 'sessionId');
|
|
1317
1633
|
await state.scopeQueue;
|
|
1634
|
+
this.assertNoCommittingSessionImport(state, sessionId);
|
|
1318
1635
|
return this.startSessionDeletion(state, scopeId, scope.scope, sessionId);
|
|
1319
1636
|
}
|
|
1320
1637
|
|
|
@@ -1636,12 +1953,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1636
1953
|
signal,
|
|
1637
1954
|
);
|
|
1638
1955
|
signal.throwIfAborted();
|
|
1639
|
-
|
|
1956
|
+
const descriptors = this.createSlashCommandDescriptors(
|
|
1640
1957
|
state,
|
|
1641
1958
|
stored,
|
|
1642
1959
|
sessionAvailable,
|
|
1643
1960
|
reversionIdleReason,
|
|
1644
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
|
+
})));
|
|
1645
1968
|
});
|
|
1646
1969
|
}
|
|
1647
1970
|
|
|
@@ -1780,6 +2103,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1780
2103
|
this.assertOpen();
|
|
1781
2104
|
this.assertStateAcceptingWork(state);
|
|
1782
2105
|
const stored = this.requireSession(state, sessionId);
|
|
2106
|
+
this.assertSessionWritable(stored);
|
|
1783
2107
|
const sessionKey = this.slashCommandSessionKey(state.storageKey, sessionId);
|
|
1784
2108
|
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
1785
2109
|
if (invocationOwner?.sessionKey === sessionKey) {
|
|
@@ -1809,6 +2133,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1809
2133
|
if (parsed.arguments.length > 0) {
|
|
1810
2134
|
throw new FlexHarnessValidationError('Slash command "/compact" does not accept arguments.');
|
|
1811
2135
|
}
|
|
2136
|
+
const ignoredOption = (['system', 'maxSteps'] as const)
|
|
2137
|
+
.find((key) => promptOptions[key] !== undefined);
|
|
2138
|
+
if (ignoredOption) {
|
|
2139
|
+
throw new FlexHarnessValidationError(
|
|
2140
|
+
`Slash command "/compact" does not support the "${ignoredOption}" option.`,
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
1812
2143
|
if (!this.agentSessionPolicy.contextCompactor) {
|
|
1813
2144
|
throw new FlexHarnessSlashCommandUnavailableError(
|
|
1814
2145
|
parsed.name,
|
|
@@ -1824,7 +2155,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1824
2155
|
options.signal,
|
|
1825
2156
|
async (signal) => {
|
|
1826
2157
|
await this.commitRevertedBranch(state, stored, scopeId, scope.scope);
|
|
1827
|
-
await this.compactStoredSession(
|
|
2158
|
+
await this.compactStoredSession(
|
|
2159
|
+
scopeId,
|
|
2160
|
+
scope.scope,
|
|
2161
|
+
state,
|
|
2162
|
+
stored,
|
|
2163
|
+
promptOptions.modelHint,
|
|
2164
|
+
signal,
|
|
2165
|
+
);
|
|
1828
2166
|
return Object.freeze({ type: 'operation', name: parsed.name });
|
|
1829
2167
|
},
|
|
1830
2168
|
);
|
|
@@ -2361,6 +2699,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2361
2699
|
}
|
|
2362
2700
|
const { scope, state } = await this.resolveState(scopeId);
|
|
2363
2701
|
const stored = this.requireSession(state, sessionId);
|
|
2702
|
+
this.assertSessionWritable(stored);
|
|
2364
2703
|
this.assertStateAcceptingWork(state);
|
|
2365
2704
|
let canonical: plugins.TAgentToolExecutionReconciliationOptions;
|
|
2366
2705
|
if (reconciliation.resolution === 'executed') {
|
|
@@ -2400,6 +2739,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2400
2739
|
this.validateRuntimeEvent(event);
|
|
2401
2740
|
const { scope, state } = await this.resolveState(scopeId);
|
|
2402
2741
|
const stored = this.requireSession(state, sessionId);
|
|
2742
|
+
this.assertSessionWritable(stored);
|
|
2403
2743
|
this.assertStateAcceptingWork(state);
|
|
2404
2744
|
try {
|
|
2405
2745
|
await this.withCompactorContext(
|
|
@@ -2411,9 +2751,15 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2411
2751
|
}
|
|
2412
2752
|
}
|
|
2413
2753
|
|
|
2414
|
-
public async compactSession(
|
|
2754
|
+
public async compactSession(
|
|
2755
|
+
scopeId: string,
|
|
2756
|
+
sessionId: string,
|
|
2757
|
+
options: IFlexCompactSessionOptions = {},
|
|
2758
|
+
): Promise<void> {
|
|
2759
|
+
const { modelHint } = validateCompactSessionOptions(options);
|
|
2415
2760
|
const { scope, state } = await this.resolveState(scopeId);
|
|
2416
2761
|
const stored = this.requireSession(state, sessionId);
|
|
2762
|
+
this.assertSessionWritable(stored);
|
|
2417
2763
|
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
2418
2764
|
if (invocationOwner?.sessionKey === this.slashCommandSessionKey(state.storageKey, sessionId)) {
|
|
2419
2765
|
throw this.trustInternalError(new FlexHarnessSlashCommandReentryError(sessionId));
|
|
@@ -2434,7 +2780,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2434
2780
|
undefined,
|
|
2435
2781
|
async (signal) => {
|
|
2436
2782
|
await this.commitRevertedBranch(state, stored, scopeId, scope.scope);
|
|
2437
|
-
await this.compactStoredSession(scopeId, scope.scope, state, stored, signal);
|
|
2783
|
+
await this.compactStoredSession(scopeId, scope.scope, state, stored, modelHint, signal);
|
|
2438
2784
|
},
|
|
2439
2785
|
);
|
|
2440
2786
|
}
|
|
@@ -2444,6 +2790,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2444
2790
|
scope: TScope,
|
|
2445
2791
|
state: IStorageState,
|
|
2446
2792
|
stored: IStoredSessionState,
|
|
2793
|
+
modelHint: string | undefined,
|
|
2447
2794
|
signal?: AbortSignal,
|
|
2448
2795
|
): Promise<void> {
|
|
2449
2796
|
try {
|
|
@@ -2453,6 +2800,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2453
2800
|
scope,
|
|
2454
2801
|
state.storageKey,
|
|
2455
2802
|
stored.session.sessionId,
|
|
2803
|
+
modelHint,
|
|
2456
2804
|
),
|
|
2457
2805
|
() => stored.agentSession.compact(signal === undefined ? {} : { abort: signal }),
|
|
2458
2806
|
);
|
|
@@ -2659,6 +3007,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2659
3007
|
signal?: AbortSignal,
|
|
2660
3008
|
): Promise<string> {
|
|
2661
3009
|
const stored = this.requireSession(state, sessionId);
|
|
3010
|
+
this.assertSessionWritable(stored);
|
|
2662
3011
|
this.assertStateAcceptingWork(state);
|
|
2663
3012
|
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
2664
3013
|
if (invocationOwner?.sessionKey === this.slashCommandSessionKey(state.storageKey, sessionId)) {
|
|
@@ -3011,6 +3360,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3011
3360
|
}
|
|
3012
3361
|
const { scope, state } = await this.resolveState(scopeId);
|
|
3013
3362
|
const stored = this.requireSession(state, sessionId);
|
|
3363
|
+
this.assertSessionWritable(stored);
|
|
3014
3364
|
this.assertStateAcceptingWork(state);
|
|
3015
3365
|
this.requireSessionAvailableForSlashCommand(state, stored, 'archive');
|
|
3016
3366
|
const compaction = compactionEventId === undefined
|
|
@@ -3210,6 +3560,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3210
3560
|
}
|
|
3211
3561
|
this.assertStateAcceptingWork(state);
|
|
3212
3562
|
const stored = this.requireSession(state, sessionId);
|
|
3563
|
+
this.assertSessionWritable(stored);
|
|
3213
3564
|
const sessionKey = this.slashCommandSessionKey(state.storageKey, sessionId);
|
|
3214
3565
|
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
3215
3566
|
if (invocationOwner?.sessionKey === sessionKey) {
|
|
@@ -4180,13 +4531,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4180
4531
|
private async resolveProjectManagementSession(
|
|
4181
4532
|
scopeId: string,
|
|
4182
4533
|
sessionId: string,
|
|
4534
|
+
access: 'read' | 'write',
|
|
4183
4535
|
): Promise<{ state: IStorageState; stored: IStoredSessionState }> {
|
|
4184
4536
|
this.requireProjectManagementTools();
|
|
4185
4537
|
const { state } = await this.resolveState(scopeId);
|
|
4186
4538
|
await state.scopeQueue;
|
|
4187
4539
|
this.assertOpen();
|
|
4188
4540
|
this.assertStateAcceptingWork(state);
|
|
4189
|
-
|
|
4541
|
+
const stored = this.requireSession(state, sessionId);
|
|
4542
|
+
if (access === 'write') this.assertSessionWritable(stored);
|
|
4543
|
+
return { state, stored };
|
|
4190
4544
|
}
|
|
4191
4545
|
|
|
4192
4546
|
private assertProjectManagementSession(
|
|
@@ -7955,6 +8309,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7955
8309
|
sessionDeletions: new Map(),
|
|
7956
8310
|
initializingSessions: new Set(),
|
|
7957
8311
|
sessionInitializations: new Map(),
|
|
8312
|
+
sessionImports: new Map(),
|
|
7958
8313
|
tombstones: new Map(snapshot.tombstones.map((entry) => [entry.sessionId, entry])),
|
|
7959
8314
|
tombstoneCleanups: new Map(),
|
|
7960
8315
|
activeRuns: new Map(),
|
|
@@ -8275,15 +8630,17 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8275
8630
|
const ambientContext = this.compactorInvocationContext.getStore();
|
|
8276
8631
|
const invocationContext = ambientContext
|
|
8277
8632
|
? (this.deferredCompactorContexts.get(ambientContext) ?? ambientContext) as
|
|
8278
|
-
|
|
8633
|
+
ICompactorInvocation<TScope>
|
|
8279
8634
|
: undefined;
|
|
8280
8635
|
if (
|
|
8281
|
-
!
|
|
8636
|
+
!ambientContext
|
|
8637
|
+
|| !invocationContext
|
|
8282
8638
|
|| invocationContext.storageKey !== state.storageKey
|
|
8283
8639
|
|| invocationContext.sessionId !== sessionId
|
|
8284
8640
|
) {
|
|
8285
8641
|
throw new Error('Agent context compaction is missing its exact FlexHarness invocation context.');
|
|
8286
8642
|
}
|
|
8643
|
+
const { modelHint, ...invocation } = invocationContext;
|
|
8287
8644
|
const filteredEvents = this.filterReversionContextEvents(stored, events);
|
|
8288
8645
|
const filteredMessages = hydrateAgentMessages(
|
|
8289
8646
|
(this.agentSessionPolicy.contextBuilder
|
|
@@ -8293,8 +8650,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8293
8650
|
);
|
|
8294
8651
|
return contextCompactor(filteredMessages, filteredEvents, {
|
|
8295
8652
|
...options,
|
|
8296
|
-
...
|
|
8297
|
-
scope:
|
|
8653
|
+
...invocation,
|
|
8654
|
+
scope: invocation.scope as TScope,
|
|
8655
|
+
...this.compactionModel(state, sessionId, ambientContext, options.reason, modelHint),
|
|
8298
8656
|
abortSignal: options.abortSignal
|
|
8299
8657
|
? AbortSignal.any([
|
|
8300
8658
|
options.abortSignal,
|
|
@@ -8365,6 +8723,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8365
8723
|
scopeId?: string,
|
|
8366
8724
|
scope?: TScope,
|
|
8367
8725
|
): Promise<boolean> {
|
|
8726
|
+
if (stored.session.origin !== undefined) {
|
|
8727
|
+
this.assertImportedSessionState(stored);
|
|
8728
|
+
return false;
|
|
8729
|
+
}
|
|
8368
8730
|
const beforeProjection = JSON.stringify({
|
|
8369
8731
|
messages: stored.messages,
|
|
8370
8732
|
stagedTerminals: stored.stagedTerminals,
|
|
@@ -9884,6 +10246,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9884
10246
|
return;
|
|
9885
10247
|
}
|
|
9886
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
|
+
}
|
|
9887
10252
|
const errors: unknown[] = [];
|
|
9888
10253
|
const listingSettlement = this.abortSlashCommandListings(storageKey, reason);
|
|
9889
10254
|
if (listingSettlement) await listingSettlement;
|
|
@@ -10016,6 +10381,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10016
10381
|
state.pendingPermissions.clear();
|
|
10017
10382
|
state.initializingSessions.clear();
|
|
10018
10383
|
state.sessionInitializations.clear();
|
|
10384
|
+
state.sessionImports.clear();
|
|
10019
10385
|
}
|
|
10020
10386
|
|
|
10021
10387
|
private async disposeInternal(): Promise<void> {
|
|
@@ -10260,8 +10626,42 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10260
10626
|
scope: TScope,
|
|
10261
10627
|
storageKey: string,
|
|
10262
10628
|
sessionId: string,
|
|
10263
|
-
|
|
10264
|
-
|
|
10629
|
+
modelHint?: string,
|
|
10630
|
+
): ICompactorInvocation<TScope> {
|
|
10631
|
+
return Object.freeze({
|
|
10632
|
+
scopeId,
|
|
10633
|
+
scope,
|
|
10634
|
+
storageKey,
|
|
10635
|
+
sessionId,
|
|
10636
|
+
...(modelHint === undefined ? {} : { modelHint }),
|
|
10637
|
+
});
|
|
10638
|
+
}
|
|
10639
|
+
|
|
10640
|
+
/**
|
|
10641
|
+
* The model the compacted work was dispatched on: the resolved model of the run whose context
|
|
10642
|
+
* overflowed, or the model hint a manual compaction was requested with. Retention states none.
|
|
10643
|
+
*/
|
|
10644
|
+
private compactionModel(
|
|
10645
|
+
state: IStorageState,
|
|
10646
|
+
sessionId: string,
|
|
10647
|
+
ambientContext: IFlexAgentContextInvocation<TScope>,
|
|
10648
|
+
reason: IFlexAgentContextCompactionOptions['reason'],
|
|
10649
|
+
modelHint: string | undefined,
|
|
10650
|
+
): Pick<IFlexAgentContextCompactionOptions<TScope>, 'resolvedModel' | 'modelHint'> {
|
|
10651
|
+
if (reason === 'manual') return modelHint === undefined ? {} : { modelHint };
|
|
10652
|
+
if (reason !== 'context-overflow') return {};
|
|
10653
|
+
const run = state.activeRuns.get(sessionId);
|
|
10654
|
+
if (
|
|
10655
|
+
!run
|
|
10656
|
+
|| (ambientContext !== run.originCompactorContext && ambientContext !== run.deferredCompactorContext)
|
|
10657
|
+
) {
|
|
10658
|
+
throw new Error('Context-overflow compaction is missing its exact FlexHarness run.');
|
|
10659
|
+
}
|
|
10660
|
+
const resolved = run.modelResolution;
|
|
10661
|
+
if (!resolved) throw new Error('Context-overflow compaction ran before its run resolved a model.');
|
|
10662
|
+
return {
|
|
10663
|
+
resolvedModel: Object.freeze({ ...resolved, identity: Object.freeze({ ...resolved.identity }) }),
|
|
10664
|
+
};
|
|
10265
10665
|
}
|
|
10266
10666
|
|
|
10267
10667
|
private withCompactorContext<TResult>(
|
|
@@ -10322,6 +10722,138 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10322
10722
|
return stored;
|
|
10323
10723
|
}
|
|
10324
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
|
+
|
|
10325
10857
|
private requireMessage(stored: IStoredSessionState, messageId: string): IFlexMessage {
|
|
10326
10858
|
const message = this.visibleMessages(stored).find((entry) => entry.messageId === messageId);
|
|
10327
10859
|
if (!message) throw new FlexHarnessNotFoundError('Message', messageId);
|