@modelprofile.com/flexharness 10.24.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/classes.codesandbox.js +1 -1
- package/dist_ts/classes.flexharness.admission.d.ts +17 -0
- package/dist_ts/classes.flexharness.admission.js +263 -0
- package/dist_ts/classes.flexharness.callbacks.d.ts +38 -0
- package/dist_ts/classes.flexharness.callbacks.js +313 -0
- package/dist_ts/classes.flexharness.compaction.d.ts +25 -0
- package/dist_ts/classes.flexharness.compaction.js +87 -0
- package/dist_ts/classes.flexharness.configurations.d.ts +11 -0
- package/dist_ts/classes.flexharness.configurations.js +137 -0
- package/dist_ts/classes.flexharness.core.d.ts +143 -0
- package/dist_ts/classes.flexharness.core.js +1538 -0
- package/dist_ts/classes.flexharness.d.ts +2 -437
- package/dist_ts/classes.flexharness.deletion.d.ts +31 -0
- package/dist_ts/classes.flexharness.deletion.js +396 -0
- package/dist_ts/classes.flexharness.imports.d.ts +24 -0
- package/dist_ts/classes.flexharness.imports.js +345 -0
- package/dist_ts/classes.flexharness.js +119 -9776
- package/dist_ts/classes.flexharness.lifecycle.d.ts +47 -0
- package/dist_ts/classes.flexharness.lifecycle.js +625 -0
- package/dist_ts/classes.flexharness.permissions.d.ts +19 -0
- package/dist_ts/classes.flexharness.permissions.js +450 -0
- package/dist_ts/classes.flexharness.projects.d.ts +55 -0
- package/dist_ts/classes.flexharness.projects.js +575 -0
- package/dist_ts/classes.flexharness.prompts.d.ts +44 -0
- package/dist_ts/classes.flexharness.prompts.js +659 -0
- package/dist_ts/classes.flexharness.providers.d.ts +18 -0
- package/dist_ts/classes.flexharness.providers.js +267 -0
- package/dist_ts/classes.flexharness.results.d.ts +26 -0
- package/dist_ts/classes.flexharness.results.js +89 -0
- package/dist_ts/classes.flexharness.reversions.d.ts +52 -0
- package/dist_ts/classes.flexharness.reversions.js +1306 -0
- package/dist_ts/classes.flexharness.runs.d.ts +63 -0
- package/dist_ts/classes.flexharness.runs.js +1058 -0
- package/dist_ts/classes.flexharness.sessions.d.ts +20 -0
- package/dist_ts/classes.flexharness.sessions.js +381 -0
- package/dist_ts/classes.flexharness.slashcommands.d.ts +40 -0
- package/dist_ts/classes.flexharness.slashcommands.js +458 -0
- package/dist_ts/classes.flexharness.subagents.d.ts +31 -0
- package/dist_ts/classes.flexharness.subagents.js +690 -0
- package/dist_ts/classes.flexharness.tooldispatch.d.ts +53 -0
- package/dist_ts/classes.flexharness.tooldispatch.js +418 -0
- package/dist_ts/classes.stores.js +1 -1
- package/dist_ts/classes.toolcatalog.js +1 -1
- package/dist_ts/errors.js +1 -1
- package/dist_ts/functions.toolpermissions.js +1 -1
- package/dist_ts/interfaces.d.ts +3 -3
- package/dist_ts/interfaces.flexharness.d.ts +455 -0
- package/dist_ts/interfaces.flexharness.js +2 -0
- package/dist_ts/utils.flexharness.d.ts +19 -0
- package/dist_ts/utils.flexharness.js +83 -0
- package/dist_ts/utils.harnessoptions.js +1 -1
- package/dist_ts/utils.json.js +1 -1
- package/dist_ts/utils.schemadeclarations.js +1 -1
- package/dist_ts/utils.sessionimport.js +1 -1
- package/dist_ts/utils.sessionmetrics.d.ts +1 -1
- package/dist_ts_chat/classes.chatsession.js +1 -1
- package/dist_ts_chat_harness/classes.transcript.js +1 -1
- package/dist_ts_chat_harness/functions.intents.js +1 -1
- package/dist_ts_chat_harness/functions.mapping.d.ts +1 -1
- package/dist_ts_chat_harness/functions.mapping.js +1 -1
- package/dist_ts_chat_harness_server/classes.chathub.d.ts +18 -7
- package/dist_ts_chat_harness_server/classes.chathub.js +65 -62
- package/dist_ts_chat_harness_server/classes.chatsubscription.d.ts +5 -3
- package/dist_ts_chat_harness_server/classes.chatsubscription.js +4 -4
- package/dist_ts_chat_harness_server/classes.scopedirectory.d.ts +17 -0
- package/dist_ts_chat_harness_server/classes.scopedirectory.js +49 -0
- package/dist_ts_chat_harness_server/classes.sessionlists.d.ts +3 -5
- package/dist_ts_chat_harness_server/classes.sessionlists.js +11 -12
- package/dist_ts_chat_harness_server/classes.turngate.d.ts +8 -2
- package/dist_ts_chat_harness_server/classes.turngate.js +22 -21
- package/dist_ts_chat_harness_server/functions.errorprojector.js +1 -1
- package/dist_ts_chat_harness_server/index.d.ts +3 -2
- package/dist_ts_chat_harness_server/index.js +3 -2
- package/dist_ts_chat_harness_server/interfaces.d.ts +35 -2
- package/dist_ts_chat_harness_server/plugins.d.ts +1 -1
- package/dist_ts_chat_harness_typedsocket/classes.clienttransport.d.ts +28 -3
- package/dist_ts_chat_harness_typedsocket/classes.clienttransport.js +71 -15
- package/dist_ts_chat_harness_typedsocket/classes.serverbinding.d.ts +5 -3
- package/dist_ts_chat_harness_typedsocket/classes.serverbinding.js +79 -16
- package/dist_ts_chat_harness_typedsocket/index.d.ts +2 -2
- package/dist_ts_chat_harness_typedsocket/index.js +1 -1
- package/dist_ts_chat_harness_typedsocket/interfaces.requests.d.ts +20 -11
- package/dist_ts_chat_web/flexchat-input.js +1 -1
- package/dist_ts_chat_web/flexchat-message.js +1 -1
- package/dist_ts_chat_web/flexchat-window.js +1 -1
- package/dist_ts_compaction/smartagent.compaction.js +1 -1
- package/dist_ts_kvm/ocr.js +1 -1
- package/dist_ts_ocr/classes.imageocr.js +1 -1
- package/dist_ts_ocr/ocr.mistral.js +1 -1
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +1 -1
- package/dist_ts_tools/tool.browser.js +1 -1
- package/dist_ts_tools/tool.filesystem.js +1 -1
- package/dist_ts_tools/tool.shell.js +1 -1
- package/dist_ts_tools_node/tool.context.js +1 -1
- package/dist_ts_typedrequest/catalog.d.ts +2 -2
- package/dist_ts_typedrequest/catalog.js +1 -1
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.admission.ts +331 -0
- package/ts/classes.flexharness.callbacks.ts +352 -0
- package/ts/classes.flexharness.compaction.ts +167 -0
- package/ts/classes.flexharness.configurations.ts +167 -0
- package/ts/classes.flexharness.core.ts +1830 -0
- package/ts/classes.flexharness.deletion.ts +616 -0
- package/ts/classes.flexharness.imports.ts +414 -0
- package/ts/classes.flexharness.lifecycle.ts +786 -0
- package/ts/classes.flexharness.permissions.ts +535 -0
- package/ts/classes.flexharness.projects.ts +1006 -0
- package/ts/classes.flexharness.prompts.ts +857 -0
- package/ts/classes.flexharness.providers.ts +322 -0
- package/ts/classes.flexharness.results.ts +98 -0
- package/ts/classes.flexharness.reversions.ts +1684 -0
- package/ts/classes.flexharness.runs.ts +1204 -0
- package/ts/classes.flexharness.sessions.ts +483 -0
- package/ts/classes.flexharness.slashcommands.ts +705 -0
- package/ts/classes.flexharness.subagents.ts +860 -0
- package/ts/classes.flexharness.tooldispatch.ts +475 -0
- package/ts/classes.flexharness.ts +147 -12567
- package/ts/interfaces.flexharness.ts +517 -0
- package/ts/utils.flexharness.ts +121 -0
- package/ts_chat_harness_server/classes.chathub.ts +68 -61
- package/ts_chat_harness_server/classes.chatsubscription.ts +7 -5
- package/ts_chat_harness_server/classes.scopedirectory.ts +67 -0
- package/ts_chat_harness_server/classes.sessionlists.ts +11 -19
- package/ts_chat_harness_server/classes.turngate.ts +28 -21
- package/ts_chat_harness_server/index.ts +4 -5
- package/ts_chat_harness_server/interfaces.ts +35 -2
- package/ts_chat_harness_server/plugins.ts +0 -3
- package/ts_chat_harness_server/readme.md +6 -4
- package/ts_chat_harness_typedsocket/classes.clienttransport.ts +92 -23
- package/ts_chat_harness_typedsocket/classes.serverbinding.ts +88 -16
- package/ts_chat_harness_typedsocket/index.ts +4 -2
- package/ts_chat_harness_typedsocket/interfaces.requests.ts +15 -11
- package/ts_chat_harness_typedsocket/readme.md +7 -3
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
FlexHarnessAbortError,
|
|
4
|
+
FlexHarnessNotFoundError,
|
|
5
|
+
FlexHarnessQueueFullError,
|
|
6
|
+
FlexHarnessSessionBusyError,
|
|
7
|
+
FlexHarnessValidationError,
|
|
8
|
+
} from './errors.js';
|
|
9
|
+
import {
|
|
10
|
+
FLEX_SESSION_IMPORT_MAX_DEPTH,
|
|
11
|
+
type IFlexBeginSessionImportOptions,
|
|
12
|
+
type IFlexImportedRun,
|
|
13
|
+
type IFlexSession,
|
|
14
|
+
type IFlexSessionImportHandle,
|
|
15
|
+
type IFlexSessionImportProgress,
|
|
16
|
+
type IFlexSessionOrigin,
|
|
17
|
+
type TFlexBeginSessionImportResult,
|
|
18
|
+
} from './interfaces.js';
|
|
19
|
+
import { cloneSerializable } from './utils.json.js';
|
|
20
|
+
import { publicSnapshot } from './utils.transfer.js';
|
|
21
|
+
import {
|
|
22
|
+
normalizeBeginSessionImportOptions,
|
|
23
|
+
normalizeImportedRunPage,
|
|
24
|
+
sessionImportIdentity,
|
|
25
|
+
validateSessionImportHandle,
|
|
26
|
+
type INormalizedSessionImportOptions,
|
|
27
|
+
} from './utils.sessionimport.js';
|
|
28
|
+
import { createSessionMetricsState } from './utils.sessionmetrics.js';
|
|
29
|
+
import type {
|
|
30
|
+
IStoredSessionState,
|
|
31
|
+
IStagedSessionImport,
|
|
32
|
+
IStorageState,
|
|
33
|
+
} from './interfaces.flexharness.js';
|
|
34
|
+
import {
|
|
35
|
+
createSessionGenerationId,
|
|
36
|
+
requireSessionGeneration,
|
|
37
|
+
combineErrors,
|
|
38
|
+
} from './utils.flexharness.js';
|
|
39
|
+
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
40
|
+
|
|
41
|
+
/** Staged imports of finished conversations as read-only sessions. */
|
|
42
|
+
export class FlexHarnessSessionImports<TScope> {
|
|
43
|
+
private readonly stagedSessionImports = new Map<string, IStagedSessionImport>();
|
|
44
|
+
|
|
45
|
+
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
46
|
+
|
|
47
|
+
public async beginSessionImport(
|
|
48
|
+
scopeId: string,
|
|
49
|
+
options: IFlexBeginSessionImportOptions,
|
|
50
|
+
): Promise<TFlexBeginSessionImportResult> {
|
|
51
|
+
this.core.assertOpen();
|
|
52
|
+
const normalized = normalizeBeginSessionImportOptions(options);
|
|
53
|
+
const { scope, state } = await this.core.resolveState(scopeId);
|
|
54
|
+
await state.scopeQueue;
|
|
55
|
+
this.core.assertOpen();
|
|
56
|
+
this.core.assertStateAcceptingWork(state);
|
|
57
|
+
const { sessionId } = normalized;
|
|
58
|
+
const identity = sessionImportIdentity(normalized.origin);
|
|
59
|
+
for (const staged of state.sessionImports.values()) {
|
|
60
|
+
if (staged.sessionId === sessionId || staged.identity === identity) {
|
|
61
|
+
throw new FlexHarnessSessionBusyError(staged.sessionId, 'is already being imported');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const stored of state.sessions.values()) {
|
|
65
|
+
const origin = stored.session.origin;
|
|
66
|
+
if (origin === undefined || sessionImportIdentity(origin) !== identity) continue;
|
|
67
|
+
const existing: TFlexBeginSessionImportResult = {
|
|
68
|
+
status: origin.digest === normalized.origin.digest ? 'already_imported' : 'digest_mismatch',
|
|
69
|
+
session: stored.session,
|
|
70
|
+
};
|
|
71
|
+
return publicSnapshot(existing);
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
state.sessions.has(sessionId)
|
|
75
|
+
|| state.tombstones.has(sessionId)
|
|
76
|
+
|| state.initializingSessions.has(sessionId)
|
|
77
|
+
) {
|
|
78
|
+
throw new FlexHarnessValidationError(`Session "${sessionId}" already exists or is being deleted.`);
|
|
79
|
+
}
|
|
80
|
+
if (normalized.parent) this.requireImportParent(state, sessionId, normalized.parent);
|
|
81
|
+
if (this.stagedSessionImports.size >= this.core.sessionImportLimits.maxOpenImports) {
|
|
82
|
+
throw new FlexHarnessQueueFullError('FlexHarness has reached its open session import limit.');
|
|
83
|
+
}
|
|
84
|
+
const staged: IStagedSessionImport = {
|
|
85
|
+
importId: `import_${plugins.crypto.randomUUID()}`,
|
|
86
|
+
scopeId,
|
|
87
|
+
state,
|
|
88
|
+
scope: scope.scope,
|
|
89
|
+
sessionId,
|
|
90
|
+
options: normalized,
|
|
91
|
+
identity,
|
|
92
|
+
runs: [],
|
|
93
|
+
runIds: new Set(),
|
|
94
|
+
messageIds: new Set(),
|
|
95
|
+
messageCount: 0,
|
|
96
|
+
bytes: 0,
|
|
97
|
+
phase: 'staging',
|
|
98
|
+
};
|
|
99
|
+
this.stagedSessionImports.set(staged.importId, staged);
|
|
100
|
+
this.armStagedImportIdleTimer(staged);
|
|
101
|
+
state.sessionImports.set(sessionId, staged);
|
|
102
|
+
state.initializingSessions.add(sessionId);
|
|
103
|
+
const started: TFlexBeginSessionImportResult = {
|
|
104
|
+
status: 'started',
|
|
105
|
+
handle: { importId: staged.importId, scopeId, sessionId },
|
|
106
|
+
};
|
|
107
|
+
return publicSnapshot(started);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public async appendImportedRuns(
|
|
111
|
+
handle: IFlexSessionImportHandle,
|
|
112
|
+
runs: readonly IFlexImportedRun[],
|
|
113
|
+
): Promise<IFlexSessionImportProgress> {
|
|
114
|
+
const staged = this.requireStagedSessionImport(handle);
|
|
115
|
+
this.core.assertOpen();
|
|
116
|
+
this.core.assertStateAcceptingWork(staged.state);
|
|
117
|
+
const page = normalizeImportedRunPage(runs, staged.sessionId, this.core.sessionImportLimits.maxPageBytes);
|
|
118
|
+
const runIds = new Set<string>();
|
|
119
|
+
const messageIds = new Set<string>();
|
|
120
|
+
for (const run of page.runs) {
|
|
121
|
+
if (staged.runIds.has(run.user.runId) || runIds.has(run.user.runId)) {
|
|
122
|
+
throw new FlexHarnessValidationError(`Imported run "${run.user.runId}" is not unique.`);
|
|
123
|
+
}
|
|
124
|
+
runIds.add(run.user.runId);
|
|
125
|
+
for (const message of [run.user, run.assistant]) {
|
|
126
|
+
if (staged.messageIds.has(message.messageId) || messageIds.has(message.messageId)) {
|
|
127
|
+
throw new FlexHarnessValidationError(`Imported message "${message.messageId}" is not unique.`);
|
|
128
|
+
}
|
|
129
|
+
messageIds.add(message.messageId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const messageCount = staged.messageCount + page.runs.length * 2;
|
|
133
|
+
if (messageCount > this.core.sessionImportLimits.maxMessagesPerSession) {
|
|
134
|
+
throw new FlexHarnessValidationError(
|
|
135
|
+
`An imported session holds at most ${this.core.sessionImportLimits.maxMessagesPerSession} messages.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const bytes = staged.bytes + page.bytes;
|
|
139
|
+
if (bytes > this.core.sessionImportLimits.maxSessionBytes) {
|
|
140
|
+
throw new FlexHarnessValidationError(
|
|
141
|
+
`An imported session holds at most ${this.core.sessionImportLimits.maxSessionBytes} message bytes.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
staged.runs.push(...page.runs);
|
|
145
|
+
for (const runId of runIds) staged.runIds.add(runId);
|
|
146
|
+
for (const messageId of messageIds) staged.messageIds.add(messageId);
|
|
147
|
+
staged.messageCount = messageCount;
|
|
148
|
+
staged.bytes = bytes;
|
|
149
|
+
this.armStagedImportIdleTimer(staged);
|
|
150
|
+
return publicSnapshot({ runs: staged.runs.length, messages: messageCount, bytes });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public async commitSessionImport(handle: IFlexSessionImportHandle): Promise<IFlexSession> {
|
|
154
|
+
const staged = this.requireStagedSessionImport(handle);
|
|
155
|
+
this.core.assertOpen();
|
|
156
|
+
const { state, scopeId, sessionId, options } = staged;
|
|
157
|
+
this.core.assertStateAcceptingWork(state);
|
|
158
|
+
if (staged.runs.length === 0) {
|
|
159
|
+
throw new FlexHarnessValidationError('A session import needs at least one run before it is committed.');
|
|
160
|
+
}
|
|
161
|
+
staged.phase = 'committing';
|
|
162
|
+
this.clearStagedImportIdleTimer(staged);
|
|
163
|
+
const scope = staged.scope as TScope;
|
|
164
|
+
let resolveInitialization!: () => void;
|
|
165
|
+
const initialization = new Promise<void>((resolve) => {
|
|
166
|
+
resolveInitialization = resolve;
|
|
167
|
+
});
|
|
168
|
+
let initializationCompleted = false;
|
|
169
|
+
const completeInitialization = () => {
|
|
170
|
+
if (initializationCompleted) return;
|
|
171
|
+
initializationCompleted = true;
|
|
172
|
+
if (state.sessionInitializations.get(sessionId) === initialization) {
|
|
173
|
+
state.sessionInitializations.delete(sessionId);
|
|
174
|
+
}
|
|
175
|
+
resolveInitialization();
|
|
176
|
+
};
|
|
177
|
+
state.sessionInitializations.set(sessionId, initialization);
|
|
178
|
+
const importedAt = new Date().toISOString();
|
|
179
|
+
const origin: IFlexSessionOrigin = {
|
|
180
|
+
kind: 'import',
|
|
181
|
+
...cloneSerializable(options.origin),
|
|
182
|
+
importedAt,
|
|
183
|
+
};
|
|
184
|
+
let metadata!: IFlexSession;
|
|
185
|
+
let reserved = false;
|
|
186
|
+
let loaded: IStoredSessionState | undefined;
|
|
187
|
+
try {
|
|
188
|
+
// A tombstone reserves the id while the projection is written, so a crash before the session
|
|
189
|
+
// is published leaves only data that the next load or deletion cleans up.
|
|
190
|
+
await this.core.mutateScope(state, () => {
|
|
191
|
+
if (state.sessions.has(sessionId) || state.tombstones.has(sessionId)) {
|
|
192
|
+
throw this.core.trustInternalError(new FlexHarnessValidationError(
|
|
193
|
+
`Session "${sessionId}" already exists or is being deleted.`,
|
|
194
|
+
));
|
|
195
|
+
}
|
|
196
|
+
const parent = options.parent
|
|
197
|
+
? this.requireImportParent(state, sessionId, options.parent)
|
|
198
|
+
: undefined;
|
|
199
|
+
metadata = {
|
|
200
|
+
scopeId,
|
|
201
|
+
sessionId,
|
|
202
|
+
sessionGenerationId: createSessionGenerationId(),
|
|
203
|
+
sessionGenerationSequence: state.revision + 1,
|
|
204
|
+
...(parent?.session.configurationRef === undefined
|
|
205
|
+
? {}
|
|
206
|
+
: { configurationRef: parent.session.configurationRef }),
|
|
207
|
+
...(options.title === undefined ? {} : { title: options.title }),
|
|
208
|
+
createdAt: options.createdAt,
|
|
209
|
+
updatedAt: options.updatedAt,
|
|
210
|
+
archivedAt: importedAt,
|
|
211
|
+
status: 'idle',
|
|
212
|
+
activity: { status: 'idle' },
|
|
213
|
+
...(options.parent === undefined
|
|
214
|
+
? {}
|
|
215
|
+
: {
|
|
216
|
+
parentSessionId: options.parent.sessionId,
|
|
217
|
+
parentRunId: options.parent.runId,
|
|
218
|
+
parentToolCallId: options.parent.toolCallId,
|
|
219
|
+
agent: options.parent.agent,
|
|
220
|
+
}),
|
|
221
|
+
depth: parent === undefined ? 0 : (parent.session.depth ?? 0) + 1,
|
|
222
|
+
origin,
|
|
223
|
+
};
|
|
224
|
+
state.tombstones.set(sessionId, {
|
|
225
|
+
sessionId,
|
|
226
|
+
...requireSessionGeneration(metadata),
|
|
227
|
+
deletedAt: importedAt,
|
|
228
|
+
rootSessionId: sessionId,
|
|
229
|
+
depth: 0,
|
|
230
|
+
imported: true,
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
reserved = true;
|
|
234
|
+
await this.core.stores.projections.save(state.storageKey, sessionId, {
|
|
235
|
+
schemaVersion: 3,
|
|
236
|
+
revision: 1,
|
|
237
|
+
metrics: createSessionMetricsState(false),
|
|
238
|
+
messages: staged.runs.flatMap((run) => [run.user, run.assistant]),
|
|
239
|
+
stagedTerminals: [],
|
|
240
|
+
reversionSegments: [],
|
|
241
|
+
revertCursor: 0,
|
|
242
|
+
excludedRunIds: [],
|
|
243
|
+
pendingReversionReleases: [],
|
|
244
|
+
}, 0);
|
|
245
|
+
const runtime = await this.core.loadSessionRuntime(state, metadata, scopeId, scope);
|
|
246
|
+
loaded = runtime;
|
|
247
|
+
await this.core.mutateScope(state, () => {
|
|
248
|
+
if (
|
|
249
|
+
state.sessions.has(sessionId)
|
|
250
|
+
|| state.tombstones.get(sessionId)?.sessionGenerationId !== metadata.sessionGenerationId
|
|
251
|
+
) {
|
|
252
|
+
throw this.core.trustInternalError(new FlexHarnessAbortError(
|
|
253
|
+
'The session import lost its reservation before it was committed.',
|
|
254
|
+
));
|
|
255
|
+
}
|
|
256
|
+
if (options.parent) this.requireImportParent(state, sessionId, options.parent);
|
|
257
|
+
state.tombstones.delete(sessionId);
|
|
258
|
+
state.sessions.set(sessionId, runtime);
|
|
259
|
+
});
|
|
260
|
+
} catch (error) {
|
|
261
|
+
const projected = this.core.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-import');
|
|
262
|
+
const errors: unknown[] = [projected];
|
|
263
|
+
if (loaded && state.sessions.get(sessionId) !== loaded) {
|
|
264
|
+
try {
|
|
265
|
+
await this.core.lifecycle.closeStoredSession(loaded);
|
|
266
|
+
} catch (closeError) {
|
|
267
|
+
this.core.lifecycle.orphanedStoredSessions.add(loaded);
|
|
268
|
+
errors.push(closeError);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// A fenced namespace cannot tell whether its last save was persisted; its next load resolves
|
|
272
|
+
// the reservation instead: a surviving tombstone is cleaned up, a published session is kept.
|
|
273
|
+
if (reserved && state.tombstones.has(sessionId) && state.lifecycle !== 'fenced') {
|
|
274
|
+
completeInitialization();
|
|
275
|
+
try {
|
|
276
|
+
await this.core.deletion.finishTombstoneCleanup(state, sessionId, scopeId, { scopeId, scope });
|
|
277
|
+
} catch (cleanupError) {
|
|
278
|
+
errors.push(this.core.projectOperationError(
|
|
279
|
+
cleanupError,
|
|
280
|
+
'persistence',
|
|
281
|
+
scopeId,
|
|
282
|
+
sessionId,
|
|
283
|
+
'session-import-cleanup',
|
|
284
|
+
));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
throw combineErrors(errors);
|
|
288
|
+
} finally {
|
|
289
|
+
this.releaseStagedSessionImport(staged);
|
|
290
|
+
completeInitialization();
|
|
291
|
+
}
|
|
292
|
+
const result = publicSnapshot(metadata);
|
|
293
|
+
this.core.emitEvent(scopeId, metadata, { type: 'session.created', session: result });
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
public async abortSessionImport(handle: IFlexSessionImportHandle): Promise<void> {
|
|
298
|
+
const staged = this.findStagedSessionImport(handle);
|
|
299
|
+
if (!staged) return;
|
|
300
|
+
if (staged.phase === 'committing') {
|
|
301
|
+
throw new FlexHarnessSessionBusyError(staged.sessionId, 'is being committed');
|
|
302
|
+
}
|
|
303
|
+
this.releaseStagedSessionImport(staged);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
public assertNoCommittingSessionImport(state: IStorageState, sessionId: string): void {
|
|
307
|
+
if (state.sessionImports.get(sessionId)?.phase === 'committing') {
|
|
308
|
+
throw new FlexHarnessSessionBusyError(sessionId, 'is being imported');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private findStagedSessionImport(
|
|
313
|
+
handle: IFlexSessionImportHandle,
|
|
314
|
+
): IStagedSessionImport | undefined {
|
|
315
|
+
const validated = validateSessionImportHandle(handle);
|
|
316
|
+
const staged = this.stagedSessionImports.get(validated.importId);
|
|
317
|
+
if (
|
|
318
|
+
staged
|
|
319
|
+
&& (staged.scopeId !== validated.scopeId || staged.sessionId !== validated.sessionId)
|
|
320
|
+
) {
|
|
321
|
+
throw new FlexHarnessValidationError('The session import handle does not match its import.');
|
|
322
|
+
}
|
|
323
|
+
return staged;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private requireStagedSessionImport(handle: IFlexSessionImportHandle): IStagedSessionImport {
|
|
327
|
+
const staged = this.findStagedSessionImport(handle);
|
|
328
|
+
if (!staged) throw new FlexHarnessNotFoundError('Session import', handle.importId);
|
|
329
|
+
if (staged.phase !== 'staging') {
|
|
330
|
+
throw new FlexHarnessSessionBusyError(staged.sessionId, 'is being committed');
|
|
331
|
+
}
|
|
332
|
+
return staged;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private armStagedImportIdleTimer(staged: IStagedSessionImport): void {
|
|
336
|
+
this.clearStagedImportIdleTimer(staged);
|
|
337
|
+
const timer = setTimeout(() => {
|
|
338
|
+
if (staged.idleTimer !== timer) return;
|
|
339
|
+
delete staged.idleTimer;
|
|
340
|
+
if (staged.phase === 'staging') this.releaseStagedSessionImport(staged);
|
|
341
|
+
}, this.core.sessionImportLimits.stagedImportIdleTimeoutMs);
|
|
342
|
+
timer.unref();
|
|
343
|
+
staged.idleTimer = timer;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private clearStagedImportIdleTimer(staged: IStagedSessionImport): void {
|
|
347
|
+
if (staged.idleTimer === undefined) return;
|
|
348
|
+
clearTimeout(staged.idleTimer);
|
|
349
|
+
delete staged.idleTimer;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
public releaseStagedSessionImport(staged: IStagedSessionImport): void {
|
|
353
|
+
this.clearStagedImportIdleTimer(staged);
|
|
354
|
+
if (this.stagedSessionImports.get(staged.importId) === staged) {
|
|
355
|
+
this.stagedSessionImports.delete(staged.importId);
|
|
356
|
+
}
|
|
357
|
+
if (staged.state.sessionImports.get(staged.sessionId) === staged) {
|
|
358
|
+
staged.state.sessionImports.delete(staged.sessionId);
|
|
359
|
+
staged.state.initializingSessions.delete(staged.sessionId);
|
|
360
|
+
}
|
|
361
|
+
staged.runs = [];
|
|
362
|
+
staged.runIds.clear();
|
|
363
|
+
staged.messageIds.clear();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* An imported child session belongs to a committed imported parent whose run `parent.runId` holds
|
|
368
|
+
* the tool call `parent.toolCallId` naming the child, and no other session claims that tool call.
|
|
369
|
+
*/
|
|
370
|
+
private requireImportParent(
|
|
371
|
+
state: IStorageState,
|
|
372
|
+
sessionId: string,
|
|
373
|
+
parent: NonNullable<INormalizedSessionImportOptions['parent']>,
|
|
374
|
+
): IStoredSessionState {
|
|
375
|
+
const parentStored = state.initializingSessions.has(parent.sessionId)
|
|
376
|
+
|| state.tombstones.has(parent.sessionId)
|
|
377
|
+
? undefined
|
|
378
|
+
: state.sessions.get(parent.sessionId);
|
|
379
|
+
if (!parentStored?.session.origin) {
|
|
380
|
+
throw this.core.trustInternalError(new FlexHarnessValidationError(
|
|
381
|
+
`The parent of imported session "${sessionId}" must be a committed imported session.`,
|
|
382
|
+
));
|
|
383
|
+
}
|
|
384
|
+
if ((parentStored.session.depth ?? 0) + 1 > FLEX_SESSION_IMPORT_MAX_DEPTH) {
|
|
385
|
+
throw this.core.trustInternalError(new FlexHarnessValidationError(
|
|
386
|
+
`Imported sessions nest at most ${FLEX_SESSION_IMPORT_MAX_DEPTH} levels deep.`,
|
|
387
|
+
));
|
|
388
|
+
}
|
|
389
|
+
const referenced = parentStored.messages.some((message) =>
|
|
390
|
+
message.role === 'assistant'
|
|
391
|
+
&& message.runId === parent.runId
|
|
392
|
+
&& message.parts.some((part) =>
|
|
393
|
+
part.type === 'tool'
|
|
394
|
+
&& part.toolCallId === parent.toolCallId
|
|
395
|
+
&& part.childSessionId === sessionId));
|
|
396
|
+
if (!referenced) {
|
|
397
|
+
throw this.core.trustInternalError(new FlexHarnessValidationError(
|
|
398
|
+
`Parent run "${parent.runId}" has no tool call "${parent.toolCallId}" whose childSessionId is "${sessionId}".`,
|
|
399
|
+
));
|
|
400
|
+
}
|
|
401
|
+
for (const stored of state.sessions.values()) {
|
|
402
|
+
if (
|
|
403
|
+
stored.session.parentSessionId === parent.sessionId
|
|
404
|
+
&& stored.session.parentRunId === parent.runId
|
|
405
|
+
&& stored.session.parentToolCallId === parent.toolCallId
|
|
406
|
+
) {
|
|
407
|
+
throw this.core.trustInternalError(new FlexHarnessValidationError(
|
|
408
|
+
`Session "${stored.session.sessionId}" already belongs to tool call "${parent.toolCallId}".`,
|
|
409
|
+
));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return parentStored;
|
|
413
|
+
}
|
|
414
|
+
}
|