@modelprofile.com/flexharness 10.23.0 → 10.24.1
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.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/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_compaction/plugins.d.ts +2 -2
- package/dist_ts_compaction/plugins.js +3 -3
- package/dist_ts_compaction/smartagent.compaction.js +6 -5
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +9 -0
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +13 -1
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +2 -2
- package/dist_ts_stores_nosqldb/classes.recordstore.js +3 -3
- package/package.json +3 -3
- package/readme.md +1 -1
- 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/readme.md +1 -1
- package/ts/utils.flexharness.ts +121 -0
- package/ts_compaction/plugins.ts +2 -2
- package/ts_compaction/smartagent.compaction.ts +5 -4
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +13 -0
- package/ts_stores_nosqldb/classes.recordstore.ts +5 -5
- package/ts_stores_nosqldb/readme.md +7 -0
|
@@ -0,0 +1,1830 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
FlexHarnessAbortError,
|
|
4
|
+
FlexHarnessClosedError,
|
|
5
|
+
FlexHarnessExternalError,
|
|
6
|
+
FlexHarnessImportedSessionError,
|
|
7
|
+
FlexHarnessNotFoundError,
|
|
8
|
+
FlexHarnessRunError,
|
|
9
|
+
FlexHarnessSessionBusyError,
|
|
10
|
+
FlexHarnessStoreConflictError,
|
|
11
|
+
FlexHarnessStoreCommitUncertainError,
|
|
12
|
+
FlexHarnessValidationError,
|
|
13
|
+
modelLimitErrorInfo,
|
|
14
|
+
} from './errors.js';
|
|
15
|
+
import type {
|
|
16
|
+
IFlexAgentSessionPolicy,
|
|
17
|
+
IFlexCallbackLimits,
|
|
18
|
+
IFlexExecutionContextHandle,
|
|
19
|
+
IFlexHarnessOptions,
|
|
20
|
+
IFlexHarnessStores,
|
|
21
|
+
IFlexMessage,
|
|
22
|
+
IFlexPartSnapshotEvent,
|
|
23
|
+
IFlexPromptQueueLimits,
|
|
24
|
+
IFlexProjectionSnapshotCurrent,
|
|
25
|
+
IFlexReversionLimits,
|
|
26
|
+
IFlexReversionSegment,
|
|
27
|
+
IFlexPendingReversionRelease,
|
|
28
|
+
IFlexResolvedScope,
|
|
29
|
+
IFlexScopeSnapshot,
|
|
30
|
+
IFlexSession,
|
|
31
|
+
IFlexSessionImportLimits,
|
|
32
|
+
IFlexSubagentDefinition,
|
|
33
|
+
IFlexTerminalProjection,
|
|
34
|
+
IFlexToolCatalogOptions,
|
|
35
|
+
IJsonObject,
|
|
36
|
+
TFlexExternalErrorProjector,
|
|
37
|
+
TFlexExternalErrorSource,
|
|
38
|
+
TFlexHarnessEvent,
|
|
39
|
+
TFlexHarnessEventListener,
|
|
40
|
+
TFlexMessagePart,
|
|
41
|
+
TFlexProjectionSnapshot,
|
|
42
|
+
TFlexReversionPolicy,
|
|
43
|
+
} from './interfaces.js';
|
|
44
|
+
import { InMemoryFlexHarnessStores } from './classes.stores.js';
|
|
45
|
+
import {
|
|
46
|
+
assertJsonSerializable,
|
|
47
|
+
cloneSerializable,
|
|
48
|
+
deepFreeze,
|
|
49
|
+
resolveJsonLimits,
|
|
50
|
+
} from './utils.json.js';
|
|
51
|
+
import { hydrateAgentMessages } from './utils.prompt.js';
|
|
52
|
+
import {
|
|
53
|
+
normalizeAgentSessionPolicy,
|
|
54
|
+
normalizeBuiltInTools,
|
|
55
|
+
normalizeSlashCommands,
|
|
56
|
+
normalizeToolCatalogOptions,
|
|
57
|
+
normalizeSubagents,
|
|
58
|
+
requireHarnessStores,
|
|
59
|
+
resolveBoundedPositiveInteger,
|
|
60
|
+
resolveCallbackLimits,
|
|
61
|
+
resolvePromptQueueLimits,
|
|
62
|
+
resolveReversionMaintenanceTimeoutMs,
|
|
63
|
+
resolveReversionLimits,
|
|
64
|
+
type INormalizedBuiltInTools,
|
|
65
|
+
type TRegisteredSlashCommand,
|
|
66
|
+
} from './utils.harnessoptions.js';
|
|
67
|
+
import {
|
|
68
|
+
jsonBytes,
|
|
69
|
+
maxMessagePageCursorBytes,
|
|
70
|
+
maxTransferIdentifierBytes,
|
|
71
|
+
publicSnapshot,
|
|
72
|
+
requireTransferIdentifier,
|
|
73
|
+
} from './utils.transfer.js';
|
|
74
|
+
import { resolveSessionImportLimits } from './utils.sessionimport.js';
|
|
75
|
+
import { validateIdentifier, validateProjectedErrorInfo } from './utils.validation.js';
|
|
76
|
+
import {
|
|
77
|
+
createSessionMetricsOperation,
|
|
78
|
+
createSessionMetricsState,
|
|
79
|
+
} from './utils.sessionmetrics.js';
|
|
80
|
+
import type {
|
|
81
|
+
TCanonicalOutcome,
|
|
82
|
+
ICompactorInvocation,
|
|
83
|
+
IStoredSessionState,
|
|
84
|
+
IStorageState,
|
|
85
|
+
IActiveRun,
|
|
86
|
+
IScopeAdmissionState,
|
|
87
|
+
IFlexMessageCursor,
|
|
88
|
+
TEventDetails,
|
|
89
|
+
} from './interfaces.flexharness.js';
|
|
90
|
+
import {
|
|
91
|
+
scopeRetirementMessage,
|
|
92
|
+
externalErrorFallback,
|
|
93
|
+
requireSessionGeneration,
|
|
94
|
+
combineErrors,
|
|
95
|
+
terminalActivityError,
|
|
96
|
+
isAbortError,
|
|
97
|
+
} from './utils.flexharness.js';
|
|
98
|
+
import { FlexHarnessResults } from './classes.flexharness.results.js';
|
|
99
|
+
import { FlexHarnessToolDispatch } from './classes.flexharness.tooldispatch.js';
|
|
100
|
+
import { FlexHarnessToolProviders } from './classes.flexharness.providers.js';
|
|
101
|
+
import { FlexHarnessCompaction } from './classes.flexharness.compaction.js';
|
|
102
|
+
import { FlexHarnessSessionConfigurations } from './classes.flexharness.configurations.js';
|
|
103
|
+
import { FlexHarnessPermissions } from './classes.flexharness.permissions.js';
|
|
104
|
+
import { FlexHarnessRunCallbacks } from './classes.flexharness.callbacks.js';
|
|
105
|
+
import { FlexHarnessDelegatedRunAdmission } from './classes.flexharness.admission.js';
|
|
106
|
+
import { FlexHarnessProjectManagement } from './classes.flexharness.projects.js';
|
|
107
|
+
import { FlexHarnessSessionImports } from './classes.flexharness.imports.js';
|
|
108
|
+
import { FlexHarnessSlashCommands } from './classes.flexharness.slashcommands.js';
|
|
109
|
+
import { FlexHarnessSubagents } from './classes.flexharness.subagents.js';
|
|
110
|
+
import { FlexHarnessPromptQueue } from './classes.flexharness.prompts.js';
|
|
111
|
+
import { FlexHarnessReversions } from './classes.flexharness.reversions.js';
|
|
112
|
+
import { FlexHarnessSessionDeletion } from './classes.flexharness.deletion.js';
|
|
113
|
+
import { FlexHarnessLifecycle } from './classes.flexharness.lifecycle.js';
|
|
114
|
+
import { FlexHarnessRuns } from './classes.flexharness.runs.js';
|
|
115
|
+
import { FlexHarnessSessions } from './classes.flexharness.sessions.js';
|
|
116
|
+
|
|
117
|
+
const defaultMaxSubagentDepth = 6;
|
|
118
|
+
const maximumMaxSubagentDepth = 8;
|
|
119
|
+
const defaultMaxSubagentCallsPerRun = 32;
|
|
120
|
+
const maximumMaxSubagentCallsPerRun = 128;
|
|
121
|
+
const repairCancellationMessage = 'The process stopped before this run completed.';
|
|
122
|
+
|
|
123
|
+
function deriveLegacySessionGenerationId(
|
|
124
|
+
storageKey: string,
|
|
125
|
+
sessionId: string,
|
|
126
|
+
createdAt: string,
|
|
127
|
+
): string {
|
|
128
|
+
const digest = plugins.crypto.createHash('sha256').update(JSON.stringify([
|
|
129
|
+
'flexharness-legacy-session-generation-v1',
|
|
130
|
+
storageKey,
|
|
131
|
+
sessionId,
|
|
132
|
+
createdAt,
|
|
133
|
+
])).digest('hex');
|
|
134
|
+
return `legacy_${digest}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The engine behind {@link FlexHarness}: its options, scope state, events and error projection, and
|
|
139
|
+
* the services that implement its API.
|
|
140
|
+
*/
|
|
141
|
+
export class FlexHarnessCore<TScope> {
|
|
142
|
+
public readonly scopeResolver: IFlexHarnessOptions<TScope>['scopeResolver'];
|
|
143
|
+
public readonly modelResolver: IFlexHarnessOptions<TScope>['modelResolver'];
|
|
144
|
+
public readonly sessionConfigurationResolver: IFlexHarnessOptions<TScope>['sessionConfigurationResolver'];
|
|
145
|
+
public readonly toolProvider: IFlexHarnessOptions<TScope>['toolProvider'];
|
|
146
|
+
public readonly resourceToolProviderResolver: IFlexHarnessOptions<TScope>['resourceToolProviderResolver'];
|
|
147
|
+
private readonly executionContextProvider: IFlexHarnessOptions<TScope>['executionContextProvider'];
|
|
148
|
+
public readonly delegatedRunAdmissionProvider: IFlexHarnessOptions<TScope>['delegatedRunAdmissionProvider'];
|
|
149
|
+
public readonly stores: IFlexHarnessStores;
|
|
150
|
+
public readonly agentSessionPolicy: IFlexAgentSessionPolicy<TScope>;
|
|
151
|
+
public readonly toolOutputLimits: Required<NonNullable<IFlexHarnessOptions<TScope>['toolOutputLimits']>>;
|
|
152
|
+
public readonly callbackLimits: Required<IFlexCallbackLimits>;
|
|
153
|
+
public readonly promptQueueLimits: Required<IFlexPromptQueueLimits>;
|
|
154
|
+
public readonly reversionLimits: Required<IFlexReversionLimits>;
|
|
155
|
+
public readonly sessionImportLimits: Required<IFlexSessionImportLimits>;
|
|
156
|
+
public readonly reversionMaintenanceTimeoutMs: number;
|
|
157
|
+
public readonly turnReversionProvider: IFlexHarnessOptions<TScope>['turnReversionProvider'];
|
|
158
|
+
public readonly reversionPolicy: TFlexReversionPolicy;
|
|
159
|
+
private readonly externalErrorProjector?: TFlexExternalErrorProjector;
|
|
160
|
+
public readonly subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
|
|
161
|
+
public readonly builtInTools: INormalizedBuiltInTools;
|
|
162
|
+
public readonly toolCatalogOptions: Readonly<IFlexToolCatalogOptions>;
|
|
163
|
+
public readonly slashCommands: ReadonlyMap<string, TRegisteredSlashCommand<TScope>>;
|
|
164
|
+
public readonly maxSubagentDepth: number;
|
|
165
|
+
public readonly maxSubagentCallsPerRun: number;
|
|
166
|
+
public readonly stateLoads = new Map<string, Promise<IStorageState>>();
|
|
167
|
+
public readonly scopeAdmissions = new Map<string, IScopeAdmissionState>();
|
|
168
|
+
public readonly listeners = new Set<TFlexHarnessEventListener>();
|
|
169
|
+
public readonly trustedInternalErrors = new WeakSet<object>();
|
|
170
|
+
public sequence = 0;
|
|
171
|
+
public closed = false;
|
|
172
|
+
public readonly results = new FlexHarnessResults<TScope>(this);
|
|
173
|
+
public readonly toolDispatch = new FlexHarnessToolDispatch<TScope>(this);
|
|
174
|
+
public readonly providers = new FlexHarnessToolProviders<TScope>(this);
|
|
175
|
+
public readonly compaction = new FlexHarnessCompaction<TScope>(this);
|
|
176
|
+
public readonly configurations = new FlexHarnessSessionConfigurations<TScope>(this);
|
|
177
|
+
public readonly permissions = new FlexHarnessPermissions<TScope>(this);
|
|
178
|
+
public readonly callbacks = new FlexHarnessRunCallbacks<TScope>(this);
|
|
179
|
+
public readonly admission = new FlexHarnessDelegatedRunAdmission<TScope>(this);
|
|
180
|
+
public readonly projects = new FlexHarnessProjectManagement<TScope>(this);
|
|
181
|
+
public readonly imports = new FlexHarnessSessionImports<TScope>(this);
|
|
182
|
+
public readonly commands = new FlexHarnessSlashCommands<TScope>(this);
|
|
183
|
+
public readonly subagentTasks = new FlexHarnessSubagents<TScope>(this);
|
|
184
|
+
public readonly prompts = new FlexHarnessPromptQueue<TScope>(this);
|
|
185
|
+
public readonly reversions = new FlexHarnessReversions<TScope>(this);
|
|
186
|
+
public readonly deletion = new FlexHarnessSessionDeletion<TScope>(this);
|
|
187
|
+
public readonly lifecycle = new FlexHarnessLifecycle<TScope>(this);
|
|
188
|
+
public readonly runs = new FlexHarnessRuns<TScope>(this);
|
|
189
|
+
public readonly sessions = new FlexHarnessSessions<TScope>(this);
|
|
190
|
+
|
|
191
|
+
constructor(options: IFlexHarnessOptions<TScope>) {
|
|
192
|
+
if (!options || typeof options !== 'object') {
|
|
193
|
+
throw new FlexHarnessValidationError('FlexHarness options are required.');
|
|
194
|
+
}
|
|
195
|
+
this.scopeResolver = options.scopeResolver;
|
|
196
|
+
this.modelResolver = options.modelResolver;
|
|
197
|
+
this.sessionConfigurationResolver = options.sessionConfigurationResolver;
|
|
198
|
+
this.toolProvider = options.toolProvider;
|
|
199
|
+
this.resourceToolProviderResolver = options.resourceToolProviderResolver;
|
|
200
|
+
this.executionContextProvider = options.executionContextProvider;
|
|
201
|
+
this.delegatedRunAdmissionProvider = options.delegatedRunAdmissionProvider;
|
|
202
|
+
this.stores = requireHarnessStores(options.stores ?? new InMemoryFlexHarnessStores());
|
|
203
|
+
this.agentSessionPolicy = normalizeAgentSessionPolicy(options.agentSessionPolicy);
|
|
204
|
+
this.toolOutputLimits = resolveJsonLimits(options.toolOutputLimits);
|
|
205
|
+
this.callbackLimits = resolveCallbackLimits(options.callbackLimits);
|
|
206
|
+
this.promptQueueLimits = resolvePromptQueueLimits(options.promptQueueLimits);
|
|
207
|
+
this.reversionLimits = resolveReversionLimits(options.reversionLimits);
|
|
208
|
+
this.sessionImportLimits = resolveSessionImportLimits(options.sessionImportLimits);
|
|
209
|
+
this.reversionMaintenanceTimeoutMs = resolveReversionMaintenanceTimeoutMs(options);
|
|
210
|
+
this.turnReversionProvider = options.turnReversionProvider;
|
|
211
|
+
this.reversionPolicy = options.reversionPolicy ?? 'transcript-optional';
|
|
212
|
+
if (!['transcript-optional', 'workspace-required'].includes(this.reversionPolicy)) {
|
|
213
|
+
throw new FlexHarnessValidationError(
|
|
214
|
+
'reversionPolicy must be "transcript-optional" or "workspace-required".',
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const protocolVersion = this.turnReversionProvider
|
|
218
|
+
&& 'protocolVersion' in this.turnReversionProvider
|
|
219
|
+
? this.turnReversionProvider.protocolVersion
|
|
220
|
+
: 1;
|
|
221
|
+
if (protocolVersion !== 1 && protocolVersion !== 2) {
|
|
222
|
+
throw new FlexHarnessValidationError('Turn reversion provider protocolVersion is invalid.');
|
|
223
|
+
}
|
|
224
|
+
if (this.reversionPolicy === 'workspace-required' && protocolVersion !== 2) {
|
|
225
|
+
throw new FlexHarnessValidationError(
|
|
226
|
+
'workspace-required reversion policy requires a protocolVersion 2 turn reversion provider.',
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
this.externalErrorProjector = options.externalErrorProjector;
|
|
230
|
+
this.subagents = normalizeSubagents(options.subagents);
|
|
231
|
+
this.builtInTools = normalizeBuiltInTools(options.builtInTools);
|
|
232
|
+
const toolCatalogOptions = normalizeToolCatalogOptions(options.toolCatalog);
|
|
233
|
+
this.toolCatalogOptions = Object.freeze({
|
|
234
|
+
declarations: toolCatalogOptions.declarations,
|
|
235
|
+
outputTypeDepth: toolCatalogOptions.outputTypeDepth,
|
|
236
|
+
describeTopMatch: toolCatalogOptions.describeTopMatch,
|
|
237
|
+
...(toolCatalogOptions.codeMode ? { codeMode: { limits: toolCatalogOptions.codeMode } } : {}),
|
|
238
|
+
});
|
|
239
|
+
this.slashCommands = normalizeSlashCommands(options.slashCommands);
|
|
240
|
+
this.maxSubagentDepth = resolveBoundedPositiveInteger(
|
|
241
|
+
options.maxSubagentDepth,
|
|
242
|
+
'maxSubagentDepth',
|
|
243
|
+
defaultMaxSubagentDepth,
|
|
244
|
+
maximumMaxSubagentDepth,
|
|
245
|
+
);
|
|
246
|
+
this.maxSubagentCallsPerRun = resolveBoundedPositiveInteger(
|
|
247
|
+
options.maxSubagentCallsPerRun,
|
|
248
|
+
'maxSubagentCallsPerRun',
|
|
249
|
+
defaultMaxSubagentCallsPerRun,
|
|
250
|
+
maximumMaxSubagentCallsPerRun,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Opens a section that changes what a view of the session shows and emits the events that
|
|
256
|
+
* describe the change. Closing is idempotent; close it once the events are emitted or the
|
|
257
|
+
* change is rolled back.
|
|
258
|
+
*/
|
|
259
|
+
public openPublication(state: IStorageState, sessionId: string): () => void {
|
|
260
|
+
let release!: () => void;
|
|
261
|
+
const section = new Promise<void>((resolve) => {
|
|
262
|
+
release = resolve;
|
|
263
|
+
});
|
|
264
|
+
let sections = state.publications.get(sessionId);
|
|
265
|
+
if (!sections) {
|
|
266
|
+
sections = new Set();
|
|
267
|
+
state.publications.set(sessionId, sections);
|
|
268
|
+
}
|
|
269
|
+
sections.add(section);
|
|
270
|
+
return () => {
|
|
271
|
+
const current = state.publications.get(sessionId);
|
|
272
|
+
if (!current?.delete(section)) return;
|
|
273
|
+
if (current.size === 0) state.publications.delete(sessionId);
|
|
274
|
+
release();
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
public subscribe(listener: TFlexHarnessEventListener): () => void {
|
|
279
|
+
this.assertOpen();
|
|
280
|
+
this.listeners.add(listener);
|
|
281
|
+
let subscribed = true;
|
|
282
|
+
return () => {
|
|
283
|
+
if (!subscribed) return;
|
|
284
|
+
subscribed = false;
|
|
285
|
+
this.listeners.delete(listener);
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
public projectExternalError(
|
|
290
|
+
run: IActiveRun,
|
|
291
|
+
error: unknown,
|
|
292
|
+
source: TFlexExternalErrorSource,
|
|
293
|
+
): Error {
|
|
294
|
+
if ((typeof error === 'object' && error !== null) || typeof error === 'function') {
|
|
295
|
+
if (
|
|
296
|
+
error === run.ownerCancellation
|
|
297
|
+
|| error === run.callbackError
|
|
298
|
+
|| this.trustedInternalErrors.has(error)
|
|
299
|
+
) return error as Error;
|
|
300
|
+
}
|
|
301
|
+
return this.projectOperationError(error, source, run.scopeId, run.sessionId, run.runId);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
public projectOperationError(
|
|
305
|
+
error: unknown,
|
|
306
|
+
source: TFlexExternalErrorSource,
|
|
307
|
+
scopeId: string,
|
|
308
|
+
sessionId: string,
|
|
309
|
+
runId: string,
|
|
310
|
+
): Error {
|
|
311
|
+
if ((typeof error === 'object' && error !== null) && this.trustedInternalErrors.has(error)) {
|
|
312
|
+
return error as Error;
|
|
313
|
+
}
|
|
314
|
+
const defaultInfo = modelLimitErrorInfo(error) ?? externalErrorFallback;
|
|
315
|
+
let info = defaultInfo;
|
|
316
|
+
if (this.externalErrorProjector) {
|
|
317
|
+
try {
|
|
318
|
+
info = validateProjectedErrorInfo(this.externalErrorProjector(error, {
|
|
319
|
+
source,
|
|
320
|
+
scopeId,
|
|
321
|
+
sessionId,
|
|
322
|
+
runId,
|
|
323
|
+
}));
|
|
324
|
+
} catch {
|
|
325
|
+
info = defaultInfo;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return this.trustInternalError(new FlexHarnessExternalError(info));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
public trustInternalError<TError extends Error>(error: TError): TError {
|
|
332
|
+
Object.freeze(error);
|
|
333
|
+
this.trustedInternalErrors.add(error);
|
|
334
|
+
return error;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
public canonicalOutcomes(events: readonly plugins.TAgentEvent[]): Map<string, TCanonicalOutcome> {
|
|
338
|
+
const outcomes = new Map<string, TCanonicalOutcome>();
|
|
339
|
+
const remember = (generationId: string, outcome: TCanonicalOutcome) => {
|
|
340
|
+
const existing = outcomes.get(generationId);
|
|
341
|
+
if (existing && existing !== outcome) {
|
|
342
|
+
throw new FlexHarnessValidationError(`Generation "${generationId}" has conflicting canonical outcomes.`);
|
|
343
|
+
}
|
|
344
|
+
outcomes.set(generationId, outcome);
|
|
345
|
+
};
|
|
346
|
+
for (const event of events) {
|
|
347
|
+
if (event.type === 'generation-outcome') remember(event.generationId, event.outcome);
|
|
348
|
+
if (event.type === 'context-compaction') {
|
|
349
|
+
for (const transaction of event.archivedTransactions ?? []) {
|
|
350
|
+
remember(transaction.generationId, transaction.outcome);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return outcomes;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
public async durableCanonicalOutcomes(
|
|
358
|
+
stored: IStoredSessionState,
|
|
359
|
+
): Promise<Map<string, TCanonicalOutcome>> {
|
|
360
|
+
const snapshot = await stored.eventStore.load(stored.session.sessionId);
|
|
361
|
+
return this.canonicalOutcomes(snapshot?.events ?? []);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private async loadState(
|
|
365
|
+
storageKey: string,
|
|
366
|
+
scopeId: string,
|
|
367
|
+
scope: TScope,
|
|
368
|
+
compactorLifecycleController: AbortController,
|
|
369
|
+
): Promise<IStorageState> {
|
|
370
|
+
const snapshot = cloneSerializable(await this.stores.scopes.load(storageKey) ?? {
|
|
371
|
+
schemaVersion: 1,
|
|
372
|
+
revision: 0,
|
|
373
|
+
sessions: [],
|
|
374
|
+
tombstones: [],
|
|
375
|
+
} satisfies IFlexScopeSnapshot);
|
|
376
|
+
let scopeChanged = false;
|
|
377
|
+
const repairedGenerationSequence = snapshot.revision + 1;
|
|
378
|
+
for (const session of snapshot.sessions) {
|
|
379
|
+
if (session.sessionGenerationId !== undefined) continue;
|
|
380
|
+
session.sessionGenerationId = deriveLegacySessionGenerationId(
|
|
381
|
+
storageKey,
|
|
382
|
+
session.sessionId,
|
|
383
|
+
session.createdAt,
|
|
384
|
+
);
|
|
385
|
+
session.sessionGenerationSequence = repairedGenerationSequence;
|
|
386
|
+
scopeChanged = true;
|
|
387
|
+
}
|
|
388
|
+
for (const tombstone of snapshot.tombstones) {
|
|
389
|
+
if (tombstone.sessionGenerationId !== undefined) continue;
|
|
390
|
+
tombstone.sessionGenerationId = deriveLegacySessionGenerationId(
|
|
391
|
+
storageKey,
|
|
392
|
+
tombstone.sessionId,
|
|
393
|
+
tombstone.deletedAt,
|
|
394
|
+
);
|
|
395
|
+
tombstone.sessionGenerationSequence = repairedGenerationSequence;
|
|
396
|
+
scopeChanged = true;
|
|
397
|
+
}
|
|
398
|
+
const state: IStorageState = {
|
|
399
|
+
storageKey,
|
|
400
|
+
compactorLifecycleController,
|
|
401
|
+
scopeIdHint: scopeId,
|
|
402
|
+
scopeContext: { scopeId, scope },
|
|
403
|
+
revision: snapshot.revision,
|
|
404
|
+
sessions: new Map(),
|
|
405
|
+
retainedSessionCleanups: new Map(),
|
|
406
|
+
sessionDeletions: new Map(),
|
|
407
|
+
initializingSessions: new Set(),
|
|
408
|
+
sessionInitializations: new Map(),
|
|
409
|
+
sessionImports: new Map(),
|
|
410
|
+
tombstones: new Map(snapshot.tombstones.map((entry) => [entry.sessionId, entry])),
|
|
411
|
+
tombstoneCleanups: new Map(),
|
|
412
|
+
activeRuns: new Map(),
|
|
413
|
+
delegatedRunAdmissionOwners: new Set(),
|
|
414
|
+
pendingPermissions: new Map(),
|
|
415
|
+
publications: new Map(),
|
|
416
|
+
scopeQueue: Promise.resolve(),
|
|
417
|
+
lifecycle: 'active',
|
|
418
|
+
fenceInProgress: false,
|
|
419
|
+
fenceAdditionalErrors: [],
|
|
420
|
+
detachedCleanups: new Set(),
|
|
421
|
+
};
|
|
422
|
+
const loadedSessions: IStoredSessionState[] = [];
|
|
423
|
+
try {
|
|
424
|
+
for (const metadata of snapshot.sessions) {
|
|
425
|
+
if (state.tombstones.has(metadata.sessionId)) continue;
|
|
426
|
+
const stored = await this.loadSessionRuntime(state, metadata, scopeId, scope);
|
|
427
|
+
state.sessions.set(metadata.sessionId, stored);
|
|
428
|
+
loadedSessions.push(stored);
|
|
429
|
+
scopeChanged = (await this.repairLoadedSession(
|
|
430
|
+
state.storageKey,
|
|
431
|
+
stored,
|
|
432
|
+
state,
|
|
433
|
+
scopeId,
|
|
434
|
+
scope,
|
|
435
|
+
)) || scopeChanged;
|
|
436
|
+
await this.reversions.drainReversionReleases(state, stored, scopeId, scope, false);
|
|
437
|
+
if (stored.projectionRevision < 0) throw new Error('Invalid projection revision.');
|
|
438
|
+
}
|
|
439
|
+
for (const stored of state.sessions.values()) {
|
|
440
|
+
let ancestorId = stored.session.parentSessionId;
|
|
441
|
+
const visited = new Set<string>([stored.session.sessionId]);
|
|
442
|
+
while (ancestorId) {
|
|
443
|
+
if (visited.has(ancestorId)) {
|
|
444
|
+
throw new FlexHarnessValidationError(
|
|
445
|
+
`Live session "${stored.session.sessionId}" has a cyclic ancestor chain.`,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
visited.add(ancestorId);
|
|
449
|
+
if (state.tombstones.has(ancestorId)) {
|
|
450
|
+
throw new FlexHarnessValidationError(
|
|
451
|
+
`Live session "${stored.session.sessionId}" descends from tombstoned ancestor "${ancestorId}".`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
ancestorId = state.sessions.get(ancestorId)?.session.parentSessionId;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const tombstoneRoots = this.deletion.orderTombstoneRootsChildFirst(
|
|
458
|
+
state,
|
|
459
|
+
[...new Set([...state.tombstones.values()].map((tombstone) =>
|
|
460
|
+
tombstone.rootSessionId ?? tombstone.sessionId))],
|
|
461
|
+
);
|
|
462
|
+
for (const rootSessionId of tombstoneRoots) {
|
|
463
|
+
if (this.deletion.descendantTombstoneRoots(state, rootSessionId).length > 0) continue;
|
|
464
|
+
try {
|
|
465
|
+
const group = this.deletion.tombstoneGroup(state, rootSessionId);
|
|
466
|
+
for (const tombstone of group) {
|
|
467
|
+
await this.reversions.releaseDeletedSessionReversions(
|
|
468
|
+
state,
|
|
469
|
+
storageKey,
|
|
470
|
+
tombstone,
|
|
471
|
+
scopeId,
|
|
472
|
+
scope,
|
|
473
|
+
);
|
|
474
|
+
await this.deletion.cleanupSessionDomains(state, tombstone);
|
|
475
|
+
}
|
|
476
|
+
for (const tombstone of group) state.tombstones.delete(tombstone.sessionId);
|
|
477
|
+
scopeChanged = true;
|
|
478
|
+
} catch (error) {
|
|
479
|
+
// A retained tombstone is retried by the next load or explicit delete call.
|
|
480
|
+
if (state.lifecycle === 'fenced') {
|
|
481
|
+
this.lifecycle.deferNamespaceDrain(state, error, { scopeId, scope });
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (scopeChanged) {
|
|
487
|
+
const next = this.createScopeSnapshot(state, state.revision + 1);
|
|
488
|
+
await this.stores.scopes.save(storageKey, next, state.revision);
|
|
489
|
+
state.revision = next.revision;
|
|
490
|
+
}
|
|
491
|
+
return state;
|
|
492
|
+
} catch (error) {
|
|
493
|
+
for (const rootSessionId of new Set([...state.tombstones.values()].map((tombstone) =>
|
|
494
|
+
tombstone.rootSessionId ?? tombstone.sessionId))) {
|
|
495
|
+
const key = JSON.stringify([storageKey, rootSessionId]);
|
|
496
|
+
this.lifecycle.orphanedTombstoneOwners.set(key, {
|
|
497
|
+
state,
|
|
498
|
+
rootSessionId,
|
|
499
|
+
scopeId,
|
|
500
|
+
scope,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
const closeResults = await Promise.allSettled(
|
|
504
|
+
loadedSessions.map((stored) => this.lifecycle.closeStoredSession(stored)),
|
|
505
|
+
);
|
|
506
|
+
const errors: unknown[] = [error];
|
|
507
|
+
for (let index = 0; index < closeResults.length; index++) {
|
|
508
|
+
const result = closeResults[index];
|
|
509
|
+
if (result.status === 'rejected') {
|
|
510
|
+
if (!this.lifecycle.storedSessionCleanupCompleted(loadedSessions[index])) {
|
|
511
|
+
this.lifecycle.orphanedStoredSessions.add(loadedSessions[index]);
|
|
512
|
+
}
|
|
513
|
+
errors.push(result.reason);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
throw combineErrors(errors);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
public createUninitializedStoredSession(
|
|
521
|
+
session: IFlexSession,
|
|
522
|
+
storageKey: string,
|
|
523
|
+
compactorLifecycleController = new AbortController(),
|
|
524
|
+
): IStoredSessionState {
|
|
525
|
+
const unavailable = new Proxy({} as plugins.IAgentSession, {
|
|
526
|
+
get() {
|
|
527
|
+
throw new Error('AgentSession is not initialized.');
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
return {
|
|
531
|
+
storageKey,
|
|
532
|
+
session,
|
|
533
|
+
messages: [],
|
|
534
|
+
stagedTerminals: [],
|
|
535
|
+
reversionSegments: [],
|
|
536
|
+
revertCursor: 0,
|
|
537
|
+
excludedRunIds: [],
|
|
538
|
+
pendingReversionReleases: [],
|
|
539
|
+
projectionSchemaVersion: 3,
|
|
540
|
+
projectionReconciliationRequired: false,
|
|
541
|
+
projectionRevision: 0,
|
|
542
|
+
projectionQueue: Promise.resolve(),
|
|
543
|
+
metrics: createSessionMetricsState(false),
|
|
544
|
+
rememberedPermissionKeys: new Set(),
|
|
545
|
+
permissionRevision: 0,
|
|
546
|
+
permissionQueue: Promise.resolve(),
|
|
547
|
+
eventStore: undefined as unknown as plugins.IAgentEventStoreV2,
|
|
548
|
+
agentSession: unavailable,
|
|
549
|
+
agentSessionAbortCompleted: true,
|
|
550
|
+
agentSessionCloseCompleted: true,
|
|
551
|
+
agentEventStoreReleased: true,
|
|
552
|
+
executionContextCloseCompleted: true,
|
|
553
|
+
jobStoreReleased: true,
|
|
554
|
+
compactorLifecycleController,
|
|
555
|
+
promptQueue: [],
|
|
556
|
+
outstandingPromptsById: new Map(),
|
|
557
|
+
terminalPromptQueueEntries: new Map(),
|
|
558
|
+
outstandingPromptBytes: 0,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
public async loadSessionRuntime(
|
|
563
|
+
state: IStorageState,
|
|
564
|
+
metadata: IFlexSession,
|
|
565
|
+
scopeId: string,
|
|
566
|
+
scope: TScope,
|
|
567
|
+
compactorLifecycleController = new AbortController(),
|
|
568
|
+
newSession = false,
|
|
569
|
+
): Promise<IStoredSessionState> {
|
|
570
|
+
const sessionId = metadata.sessionId;
|
|
571
|
+
const compactorContext = this.compaction.createCompactorContext(
|
|
572
|
+
scopeId,
|
|
573
|
+
scope,
|
|
574
|
+
state.storageKey,
|
|
575
|
+
sessionId,
|
|
576
|
+
);
|
|
577
|
+
const [projectionResult, permissionResult, eventStoreResult, jobStoreResult] = await Promise.allSettled([
|
|
578
|
+
this.stores.projections.load(state.storageKey, sessionId),
|
|
579
|
+
this.stores.permissions.load(state.storageKey, sessionId),
|
|
580
|
+
this.stores.agentEvents.getStore(state.storageKey, sessionId),
|
|
581
|
+
this.stores.jobs.getStore(state.storageKey, sessionId),
|
|
582
|
+
]);
|
|
583
|
+
const acquisitionErrors = [projectionResult, permissionResult, eventStoreResult, jobStoreResult]
|
|
584
|
+
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
585
|
+
.map((result) => result.reason);
|
|
586
|
+
if (
|
|
587
|
+
projectionResult.status === 'rejected'
|
|
588
|
+
|| permissionResult.status === 'rejected'
|
|
589
|
+
|| eventStoreResult.status === 'rejected'
|
|
590
|
+
|| jobStoreResult.status === 'rejected'
|
|
591
|
+
) {
|
|
592
|
+
if (eventStoreResult.status === 'fulfilled') {
|
|
593
|
+
try {
|
|
594
|
+
await this.lifecycle.releasePartialProviderSession('agentEvents', state.storageKey, sessionId);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
acquisitionErrors.push(error);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (jobStoreResult.status === 'fulfilled') {
|
|
600
|
+
try {
|
|
601
|
+
await this.lifecycle.releasePartialProviderSession('jobs', state.storageKey, sessionId);
|
|
602
|
+
} catch (error) {
|
|
603
|
+
acquisitionErrors.push(error);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
throw combineErrors(acquisitionErrors);
|
|
607
|
+
}
|
|
608
|
+
const projection = projectionResult.value;
|
|
609
|
+
const legacyProjection = projection?.schemaVersion === 2 ? projection : undefined;
|
|
610
|
+
const currentProjection = projection?.schemaVersion === 3 ? projection : undefined;
|
|
611
|
+
const reversionSegments: IFlexReversionSegment[] = currentProjection
|
|
612
|
+
? cloneSerializable(currentProjection.reversionSegments)
|
|
613
|
+
: (legacyProjection?.reversionSegments ?? []).map((segment): IFlexReversionSegment => ({
|
|
614
|
+
...cloneSerializable(segment),
|
|
615
|
+
protocolVersion: 1,
|
|
616
|
+
provenance: segment.workspaceCaptured ? 'workspace' : 'transcript',
|
|
617
|
+
...(segment.workspaceCaptured
|
|
618
|
+
? { disposition: segment.status === 'capturing' ? 'pending' : 'revertible' }
|
|
619
|
+
: {}),
|
|
620
|
+
}));
|
|
621
|
+
const pendingReversion = currentProjection?.pendingReversion
|
|
622
|
+
?? (legacyProjection?.pendingReversion?.kind === 'capture'
|
|
623
|
+
? { ...legacyProjection.pendingReversion, protocolVersion: 1 as const }
|
|
624
|
+
: legacyProjection?.pendingReversion);
|
|
625
|
+
const pendingReversionReleases: IFlexPendingReversionRelease[] = currentProjection
|
|
626
|
+
? cloneSerializable(currentProjection.pendingReversionReleases)
|
|
627
|
+
: (legacyProjection?.pendingReversionReleases ?? []).map((release) => ({
|
|
628
|
+
...cloneSerializable(release),
|
|
629
|
+
protocolVersion: 1,
|
|
630
|
+
}));
|
|
631
|
+
const permission = permissionResult.value;
|
|
632
|
+
const eventStore = eventStoreResult.value;
|
|
633
|
+
const jobStore = jobStoreResult.value;
|
|
634
|
+
let executionContextHandle: IFlexExecutionContextHandle | undefined;
|
|
635
|
+
try {
|
|
636
|
+
if (this.executionContextProvider) {
|
|
637
|
+
executionContextHandle = await this.compaction.withCompactorContext(
|
|
638
|
+
compactorContext,
|
|
639
|
+
() => this.executionContextProvider!.provideExecutionContext({
|
|
640
|
+
scopeId,
|
|
641
|
+
scope,
|
|
642
|
+
storageKey: state.storageKey,
|
|
643
|
+
sessionId,
|
|
644
|
+
jobStore,
|
|
645
|
+
}),
|
|
646
|
+
);
|
|
647
|
+
if (
|
|
648
|
+
executionContextHandle
|
|
649
|
+
&& (!executionContextHandle.context
|
|
650
|
+
|| (executionContextHandle.close !== undefined && typeof executionContextHandle.close !== 'function'))
|
|
651
|
+
) {
|
|
652
|
+
throw new FlexHarnessValidationError('Execution context provider returned an invalid handle.');
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const stored: IStoredSessionState = {
|
|
656
|
+
storageKey: state.storageKey,
|
|
657
|
+
compactorContext,
|
|
658
|
+
session: cloneSerializable(metadata),
|
|
659
|
+
messages: cloneSerializable(projection?.messages ?? []),
|
|
660
|
+
stagedTerminals: cloneSerializable(projection?.stagedTerminals ?? []),
|
|
661
|
+
reversionSegments,
|
|
662
|
+
revertCursor: currentProjection?.revertCursor ?? legacyProjection?.revertCursor ?? 0,
|
|
663
|
+
excludedRunIds: cloneSerializable(
|
|
664
|
+
currentProjection?.excludedRunIds ?? legacyProjection?.excludedRunIds ?? [],
|
|
665
|
+
),
|
|
666
|
+
...(pendingReversion === undefined
|
|
667
|
+
? {}
|
|
668
|
+
: { pendingReversion: cloneSerializable(pendingReversion) }),
|
|
669
|
+
pendingReversionReleases,
|
|
670
|
+
projectionSchemaVersion: projection?.schemaVersion ?? 3,
|
|
671
|
+
projectionReconciliationRequired: false,
|
|
672
|
+
...(projection
|
|
673
|
+
? { projectionBaseline: cloneSerializable(projection) }
|
|
674
|
+
: {}),
|
|
675
|
+
projectionRevision: projection?.revision ?? 0,
|
|
676
|
+
projectionQueue: Promise.resolve(),
|
|
677
|
+
metrics: cloneSerializable(currentProjection?.metrics ?? createSessionMetricsState(newSession)),
|
|
678
|
+
rememberedPermissionKeys: new Set(permission?.rememberedPermissionKeys ?? []),
|
|
679
|
+
permissionRevision: permission?.revision ?? 0,
|
|
680
|
+
permissionQueue: Promise.resolve(),
|
|
681
|
+
eventStore,
|
|
682
|
+
agentSession: undefined as unknown as plugins.IAgentSession,
|
|
683
|
+
agentSessionAbortCompleted: false,
|
|
684
|
+
agentSessionCloseCompleted: false,
|
|
685
|
+
agentEventStoreReleased: this.stores.agentEvents.releaseSession === undefined,
|
|
686
|
+
executionContextHandle,
|
|
687
|
+
executionContextCloseCompleted: executionContextHandle?.close === undefined,
|
|
688
|
+
jobStoreReleased: this.stores.jobs.releaseSession === undefined,
|
|
689
|
+
jobs: executionContextHandle?.context.jobs,
|
|
690
|
+
compactorLifecycleController,
|
|
691
|
+
promptQueue: [],
|
|
692
|
+
outstandingPromptsById: new Map(),
|
|
693
|
+
terminalPromptQueueEntries: new Map(),
|
|
694
|
+
outstandingPromptBytes: 0,
|
|
695
|
+
};
|
|
696
|
+
if (currentProjection?.metrics === undefined
|
|
697
|
+
|| stored.metrics.pendingGenerationId !== undefined
|
|
698
|
+
|| stored.metrics.pendingCompactionId !== undefined
|
|
699
|
+
|| (metadata.origin !== undefined && stored.metrics.lifetimeUsageComplete)) {
|
|
700
|
+
await this.mutateProjection(state, stored, () => {
|
|
701
|
+
if (stored.metrics.pendingGenerationId !== undefined || stored.metrics.pendingCompactionId !== undefined
|
|
702
|
+
|| metadata.origin !== undefined) {
|
|
703
|
+
stored.metrics.lifetimeUsageComplete = false;
|
|
704
|
+
}
|
|
705
|
+
delete stored.metrics.pendingGenerationId;
|
|
706
|
+
delete stored.metrics.pendingCompactionId;
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
const {
|
|
710
|
+
contextCompactor,
|
|
711
|
+
...agentSessionPolicy
|
|
712
|
+
} = this.agentSessionPolicy;
|
|
713
|
+
const executionContext = executionContextHandle?.context;
|
|
714
|
+
const jobContext = executionContext?.jobs;
|
|
715
|
+
const contextualExecutionContext = executionContext && jobContext?.subscribe
|
|
716
|
+
? {
|
|
717
|
+
...executionContext,
|
|
718
|
+
jobs: {
|
|
719
|
+
start: jobContext.start.bind(jobContext),
|
|
720
|
+
get: jobContext.get.bind(jobContext),
|
|
721
|
+
list: jobContext.list.bind(jobContext),
|
|
722
|
+
...(jobContext.abort ? { abort: jobContext.abort.bind(jobContext) } : {}),
|
|
723
|
+
subscribe: (listener: Parameters<NonNullable<typeof jobContext.subscribe>>[0]) =>
|
|
724
|
+
jobContext.subscribe!((event) =>
|
|
725
|
+
this.compaction.withCompactorContext(compactorContext, () => listener(event))),
|
|
726
|
+
},
|
|
727
|
+
}
|
|
728
|
+
: executionContext;
|
|
729
|
+
const agentSessionOptions: plugins.IAgentSessionOptions & {
|
|
730
|
+
transactionOutcomeErrorProjector: (error: unknown) => string;
|
|
731
|
+
} = {
|
|
732
|
+
...agentSessionPolicy,
|
|
733
|
+
sessionId,
|
|
734
|
+
eventStore,
|
|
735
|
+
executionContext: contextualExecutionContext,
|
|
736
|
+
contextBuilder: ({ events }) => hydrateAgentMessages(
|
|
737
|
+
(this.agentSessionPolicy.contextBuilder ?? ((options) => plugins.buildModelMessages(options.events)))({
|
|
738
|
+
events: this.reversions.filterReversionContextEvents(stored, events),
|
|
739
|
+
}),
|
|
740
|
+
),
|
|
741
|
+
...(contextCompactor
|
|
742
|
+
? {
|
|
743
|
+
contextCompactor: async (messages, events, options) => {
|
|
744
|
+
const ambientContext = this.compaction.compactorInvocationContext.getStore();
|
|
745
|
+
const invocationContext = ambientContext
|
|
746
|
+
? (this.compaction.deferredCompactorContexts.get(ambientContext) ?? ambientContext) as
|
|
747
|
+
ICompactorInvocation<TScope>
|
|
748
|
+
: undefined;
|
|
749
|
+
if (
|
|
750
|
+
!ambientContext
|
|
751
|
+
|| !invocationContext
|
|
752
|
+
|| invocationContext.storageKey !== state.storageKey
|
|
753
|
+
|| invocationContext.sessionId !== sessionId
|
|
754
|
+
) {
|
|
755
|
+
throw new Error('Agent context compaction is missing its exact FlexHarness invocation context.');
|
|
756
|
+
}
|
|
757
|
+
const { modelHint, ...invocation } = invocationContext;
|
|
758
|
+
const filteredEvents = this.reversions.filterReversionContextEvents(stored, events);
|
|
759
|
+
const filteredMessages = hydrateAgentMessages(
|
|
760
|
+
(this.agentSessionPolicy.contextBuilder
|
|
761
|
+
?? ((contextOptions) => plugins.buildModelMessages(contextOptions.events)))({
|
|
762
|
+
events: filteredEvents,
|
|
763
|
+
}),
|
|
764
|
+
);
|
|
765
|
+
const operation = createSessionMetricsOperation(plugins.crypto.randomUUID(), 'compaction');
|
|
766
|
+
try {
|
|
767
|
+
await this.mutateProjection(state, stored, () => {
|
|
768
|
+
if (stored.metrics.pendingCompactionId !== undefined || stored.metricsFailure !== undefined) {
|
|
769
|
+
throw new Error('Session metrics compaction ownership is unresolved.');
|
|
770
|
+
}
|
|
771
|
+
stored.metrics.pendingCompactionId = operation.id;
|
|
772
|
+
});
|
|
773
|
+
} catch (error) {
|
|
774
|
+
// No compactor ran. Reconcile a known committed reservation without inventing spend.
|
|
775
|
+
// Unknown outcomes remain fenced and are repaired conservatively on namespace reload.
|
|
776
|
+
if (state.lifecycle !== 'fenced' && stored.metrics.pendingCompactionId === operation.id) {
|
|
777
|
+
try {
|
|
778
|
+
await this.mutateProjection(state, stored, () => {
|
|
779
|
+
delete stored.metrics.pendingCompactionId;
|
|
780
|
+
}, true);
|
|
781
|
+
} catch (cleanupError) {
|
|
782
|
+
stored.metricsFailure ??= cleanupError;
|
|
783
|
+
state.lifecycle = 'fenced';
|
|
784
|
+
throw combineErrors([error, cleanupError]);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
throw error;
|
|
788
|
+
}
|
|
789
|
+
stored.metricsCompaction = operation;
|
|
790
|
+
try {
|
|
791
|
+
return await contextCompactor(filteredMessages, filteredEvents, {
|
|
792
|
+
...options,
|
|
793
|
+
...invocation,
|
|
794
|
+
scope: invocation.scope as TScope,
|
|
795
|
+
...this.compaction.compactionModel(state, sessionId, ambientContext, options.reason, modelHint),
|
|
796
|
+
abortSignal: options.abortSignal
|
|
797
|
+
? AbortSignal.any([
|
|
798
|
+
options.abortSignal,
|
|
799
|
+
state.compactorLifecycleController.signal,
|
|
800
|
+
stored.compactorLifecycleController.signal,
|
|
801
|
+
])
|
|
802
|
+
: AbortSignal.any([
|
|
803
|
+
state.compactorLifecycleController.signal,
|
|
804
|
+
stored.compactorLifecycleController.signal,
|
|
805
|
+
]),
|
|
806
|
+
});
|
|
807
|
+
} finally {
|
|
808
|
+
try {
|
|
809
|
+
await this.callbacks.settleMetricsOperation(state, stored, operation);
|
|
810
|
+
} finally {
|
|
811
|
+
delete stored.metricsCompaction;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
}
|
|
816
|
+
: {}),
|
|
817
|
+
transactionOutcomeErrorProjector: () => 'The model operation failed.',
|
|
818
|
+
onToken: (delta) => this.callbacks.onTextDelta(state, sessionId, delta),
|
|
819
|
+
onReasoningStart: (id) => this.callbacks.onReasoningStart(state, sessionId, id),
|
|
820
|
+
onReasoningDelta: (id, delta) => this.callbacks.onReasoningDelta(state, sessionId, id, delta),
|
|
821
|
+
onReasoningEnd: (id, text) => this.callbacks.onReasoningEnd(state, sessionId, id, text),
|
|
822
|
+
onToolCallStart: (event) => this.callbacks.onToolStart(state, sessionId, event),
|
|
823
|
+
onToolCallUpdate: (event) => this.callbacks.onToolUpdate(state, sessionId, event),
|
|
824
|
+
onToolCallFinish: (event) => this.callbacks.onToolFinish(state, sessionId, event),
|
|
825
|
+
onRetry: (event) => this.callbacks.onRetry(state, sessionId, event),
|
|
826
|
+
onUsage: (event) => this.callbacks.onModelCallUsage(state, stored, event),
|
|
827
|
+
};
|
|
828
|
+
stored.agentSession = await this.compaction.withCompactorContext(
|
|
829
|
+
compactorContext,
|
|
830
|
+
() => plugins.AgentSession.create(agentSessionOptions),
|
|
831
|
+
);
|
|
832
|
+
return stored;
|
|
833
|
+
} catch (error) {
|
|
834
|
+
const cleanupErrors: unknown[] = [];
|
|
835
|
+
let executionContextClosed = true;
|
|
836
|
+
if (executionContextHandle && typeof executionContextHandle.close === 'function') {
|
|
837
|
+
try {
|
|
838
|
+
await executionContextHandle.close();
|
|
839
|
+
} catch (closeError) {
|
|
840
|
+
executionContextClosed = false;
|
|
841
|
+
this.lifecycle.orphanedExecutionContexts.set(executionContextHandle, {
|
|
842
|
+
storageKey: state.storageKey,
|
|
843
|
+
sessionId,
|
|
844
|
+
});
|
|
845
|
+
cleanupErrors.push(closeError);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
await this.lifecycle.releasePartialProviderSession('agentEvents', state.storageKey, sessionId);
|
|
850
|
+
} catch (releaseError) {
|
|
851
|
+
cleanupErrors.push(releaseError);
|
|
852
|
+
}
|
|
853
|
+
if (executionContextClosed) {
|
|
854
|
+
try {
|
|
855
|
+
await this.lifecycle.releasePartialProviderSession('jobs', state.storageKey, sessionId);
|
|
856
|
+
} catch (releaseError) {
|
|
857
|
+
cleanupErrors.push(releaseError);
|
|
858
|
+
}
|
|
859
|
+
} else {
|
|
860
|
+
this.lifecycle.retainPartialProviderSession('jobs', state.storageKey, sessionId);
|
|
861
|
+
}
|
|
862
|
+
throw cleanupErrors.length === 0 ? error : combineErrors([error, ...cleanupErrors]);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
private async repairLoadedSession(
|
|
867
|
+
storageKey: string,
|
|
868
|
+
stored: IStoredSessionState,
|
|
869
|
+
state?: IStorageState,
|
|
870
|
+
scopeId?: string,
|
|
871
|
+
scope?: TScope,
|
|
872
|
+
): Promise<boolean> {
|
|
873
|
+
if (stored.session.origin !== undefined) {
|
|
874
|
+
this.assertImportedSessionState(stored);
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
const beforeProjection = JSON.stringify({
|
|
878
|
+
messages: stored.messages,
|
|
879
|
+
stagedTerminals: stored.stagedTerminals,
|
|
880
|
+
reversionSegments: stored.reversionSegments,
|
|
881
|
+
revertCursor: stored.revertCursor,
|
|
882
|
+
excludedRunIds: stored.excludedRunIds,
|
|
883
|
+
pendingReversion: stored.pendingReversion,
|
|
884
|
+
pendingReversionReleases: stored.pendingReversionReleases,
|
|
885
|
+
});
|
|
886
|
+
const beforeSession = JSON.stringify(stored.session);
|
|
887
|
+
if (
|
|
888
|
+
!this.turnReversionProvider
|
|
889
|
+
&& (
|
|
890
|
+
stored.reversionSegments.some((segment) => segment.captureId)
|
|
891
|
+
|| stored.pendingReversionReleases.length > 0
|
|
892
|
+
|| stored.pendingReversion?.kind === 'capture'
|
|
893
|
+
)
|
|
894
|
+
) {
|
|
895
|
+
throw new FlexHarnessValidationError(
|
|
896
|
+
'Capture-backed session recovery requires its turn reversion provider.',
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
const requiredProtocols = new Set<number>([
|
|
900
|
+
...stored.reversionSegments
|
|
901
|
+
.filter((segment) => segment.captureId !== undefined)
|
|
902
|
+
.map((segment) => segment.protocolVersion),
|
|
903
|
+
...stored.pendingReversionReleases.map((release) => release.protocolVersion),
|
|
904
|
+
...(stored.pendingReversion?.kind === 'capture'
|
|
905
|
+
? [stored.pendingReversion.protocolVersion]
|
|
906
|
+
: []),
|
|
907
|
+
]);
|
|
908
|
+
const providerProtocol = this.reversions.reversionProviderProtocolVersion();
|
|
909
|
+
if (requiredProtocols.size > 0 && (
|
|
910
|
+
requiredProtocols.size !== 1
|
|
911
|
+
|| !requiredProtocols.has(providerProtocol ?? 0)
|
|
912
|
+
)) {
|
|
913
|
+
throw new FlexHarnessValidationError(
|
|
914
|
+
`Capture-backed session recovery requires protocolVersion ${[...requiredProtocols].join(', ')}.`,
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
if (stored.pendingReversion?.kind === 'capture' && stored.pendingReversionReleases.length > 0) {
|
|
918
|
+
if (!state || scopeId === undefined || scope === undefined) {
|
|
919
|
+
throw new FlexHarnessValidationError('Pending capture recovery is missing its scope context.');
|
|
920
|
+
}
|
|
921
|
+
await this.reversions.drainReversionReleases(state, stored, scopeId, scope, false);
|
|
922
|
+
this.assertStateAcceptingWork(state);
|
|
923
|
+
}
|
|
924
|
+
const outcomes = this.canonicalOutcomes(stored.agentSession.getEvents());
|
|
925
|
+
const firstHiddenCandidate = this.reversions.completedReversionCandidates(stored)[stored.revertCursor];
|
|
926
|
+
const reversionVisibilityBoundary = firstHiddenCandidate
|
|
927
|
+
? stored.reversionSegments.findIndex((segment) => segment.runId === firstHiddenCandidate.runId)
|
|
928
|
+
: stored.reversionSegments.length;
|
|
929
|
+
const stages = new Map(stored.stagedTerminals.map((terminal) => [terminal.runId, terminal]));
|
|
930
|
+
const runIds = new Set([
|
|
931
|
+
...stored.messages.map((message) => message.runId),
|
|
932
|
+
...stored.stagedTerminals.map((terminal) => terminal.runId),
|
|
933
|
+
]);
|
|
934
|
+
let latestTerminal: IFlexTerminalProjection | undefined;
|
|
935
|
+
for (const runId of runIds) {
|
|
936
|
+
const outcome = outcomes.get(runId);
|
|
937
|
+
const stage = stages.get(runId);
|
|
938
|
+
if (outcome === 'accepted' && stage?.status === 'completed') {
|
|
939
|
+
this.runs.replaceVisibleRun(stored, stage);
|
|
940
|
+
latestTerminal = stage;
|
|
941
|
+
} else if (outcome !== 'accepted') {
|
|
942
|
+
const visible = stored.messages.filter((message) => message.runId === runId);
|
|
943
|
+
if (visible.length > 0 || stage) {
|
|
944
|
+
const status = outcome === 'rejected' ? 'failed' : 'cancelled';
|
|
945
|
+
const terminal = this.repairTerminal(stored.session.sessionId, runId, visible, stage, status);
|
|
946
|
+
this.runs.replaceVisibleRun(stored, terminal);
|
|
947
|
+
latestTerminal = terminal;
|
|
948
|
+
}
|
|
949
|
+
} else {
|
|
950
|
+
const visibleSuccess = stored.messages.some((message) =>
|
|
951
|
+
message.runId === runId && message.status === 'completed');
|
|
952
|
+
if (visibleSuccess) {
|
|
953
|
+
latestTerminal = this.repairTerminal(
|
|
954
|
+
stored.session.sessionId,
|
|
955
|
+
runId,
|
|
956
|
+
stored.messages.filter((message) => message.runId === runId),
|
|
957
|
+
undefined,
|
|
958
|
+
'completed',
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
stored.stagedTerminals = stored.stagedTerminals.filter((terminal) => !outcomes.has(terminal.runId));
|
|
964
|
+
if (stored.stagedTerminals.length > 0) stored.stagedTerminals = [];
|
|
965
|
+
const activeEvents = stored.agentSession.getEvents();
|
|
966
|
+
const activeEventIds = new Set(activeEvents.map((event) => event.id));
|
|
967
|
+
for (let index = 0; index < stored.reversionSegments.length; index++) {
|
|
968
|
+
const segment = stored.reversionSegments[index];
|
|
969
|
+
if (segment.eventIds.length > 0 && !segment.eventIds.some((id) => activeEventIds.has(id))) {
|
|
970
|
+
segment.contextAvailable = false;
|
|
971
|
+
}
|
|
972
|
+
if (segment.status !== 'capturing') {
|
|
973
|
+
const outcome = outcomes.get(segment.runId);
|
|
974
|
+
if (outcome === 'accepted') segment.status = 'completed';
|
|
975
|
+
else if (outcome === 'rejected') segment.status = 'failed';
|
|
976
|
+
else if (outcome === 'interrupted' || segment.contextAvailable) segment.status = 'cancelled';
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
stored.revertCursor = stored.reversionSegments
|
|
980
|
+
.slice(0, reversionVisibilityBoundary)
|
|
981
|
+
.filter((segment) => segment.status === 'completed')
|
|
982
|
+
.length;
|
|
983
|
+
this.reversions.commitArchivedHiddenBranchState(stored);
|
|
984
|
+
this.reversions.pruneReversionState(stored, false, true);
|
|
985
|
+
if (stored.pendingReversion?.kind === 'apply') {
|
|
986
|
+
if (!state || scopeId === undefined || scope === undefined) {
|
|
987
|
+
throw new FlexHarnessValidationError('Pending reversion recovery is missing its scope context.');
|
|
988
|
+
}
|
|
989
|
+
await this.reversions.resumePendingApply(state, stored, scopeId, scope);
|
|
990
|
+
} else if (stored.pendingReversion?.kind === 'capture') {
|
|
991
|
+
if (!this.turnReversionProvider || !state || scopeId === undefined || scope === undefined) {
|
|
992
|
+
throw new FlexHarnessValidationError('Pending capture recovery requires its reversion provider.');
|
|
993
|
+
}
|
|
994
|
+
await this.reversions.recoverPendingCapture(state, stored, scopeId, scope, outcomes);
|
|
995
|
+
}
|
|
996
|
+
if (latestTerminal) {
|
|
997
|
+
const completedAt = latestTerminal.assistantMessage.completedAt ?? new Date().toISOString();
|
|
998
|
+
stored.session.status = latestTerminal.status === 'completed' ? 'idle' : latestTerminal.status;
|
|
999
|
+
stored.session.activity = {
|
|
1000
|
+
runId: latestTerminal.runId,
|
|
1001
|
+
status: latestTerminal.status === 'completed' ? 'idle' : latestTerminal.status,
|
|
1002
|
+
completedAt,
|
|
1003
|
+
...terminalActivityError(latestTerminal.assistantMessage),
|
|
1004
|
+
};
|
|
1005
|
+
stored.session.updatedAt = completedAt;
|
|
1006
|
+
} else if (['scheduled', 'running', 'waiting_permission'].includes(stored.session.status)) {
|
|
1007
|
+
const timestamp = new Date().toISOString();
|
|
1008
|
+
stored.session.status = 'cancelled';
|
|
1009
|
+
stored.session.activity = {
|
|
1010
|
+
...(stored.session.activity.runId ? { runId: stored.session.activity.runId } : {}),
|
|
1011
|
+
status: 'cancelled',
|
|
1012
|
+
completedAt: timestamp,
|
|
1013
|
+
error: repairCancellationMessage,
|
|
1014
|
+
};
|
|
1015
|
+
stored.session.updatedAt = timestamp;
|
|
1016
|
+
}
|
|
1017
|
+
const projectionChanged = beforeProjection !== JSON.stringify({
|
|
1018
|
+
messages: stored.messages,
|
|
1019
|
+
stagedTerminals: stored.stagedTerminals,
|
|
1020
|
+
reversionSegments: stored.reversionSegments,
|
|
1021
|
+
revertCursor: stored.revertCursor,
|
|
1022
|
+
excludedRunIds: stored.excludedRunIds,
|
|
1023
|
+
pendingReversion: stored.pendingReversion,
|
|
1024
|
+
pendingReversionReleases: stored.pendingReversionReleases,
|
|
1025
|
+
});
|
|
1026
|
+
if (projectionChanged) {
|
|
1027
|
+
const expected = stored.projectionRevision;
|
|
1028
|
+
const snapshot = this.createProjectionSnapshot(stored, expected + 1);
|
|
1029
|
+
await this.stores.projections.save(
|
|
1030
|
+
storageKey,
|
|
1031
|
+
stored.session.sessionId,
|
|
1032
|
+
snapshot,
|
|
1033
|
+
expected,
|
|
1034
|
+
);
|
|
1035
|
+
stored.projectionSchemaVersion = 3;
|
|
1036
|
+
delete stored.projectionBaseline;
|
|
1037
|
+
stored.projectionReconciliationRequired = false;
|
|
1038
|
+
stored.projectionRevision = snapshot.revision;
|
|
1039
|
+
}
|
|
1040
|
+
return beforeSession !== JSON.stringify(stored.session);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
private repairTerminal(
|
|
1044
|
+
sessionId: string,
|
|
1045
|
+
runId: string,
|
|
1046
|
+
visible: IFlexMessage[],
|
|
1047
|
+
stage: IFlexTerminalProjection | undefined,
|
|
1048
|
+
status: 'completed' | 'failed' | 'cancelled',
|
|
1049
|
+
): IFlexTerminalProjection {
|
|
1050
|
+
const timestamp = new Date().toISOString();
|
|
1051
|
+
// A steered run projects its prompt first and its steers after it; its last assistant
|
|
1052
|
+
// message is the one that was still answering.
|
|
1053
|
+
const sourceUser = stage?.userMessage
|
|
1054
|
+
?? visible.find((message) => message.role === 'user' && message.steerId === undefined);
|
|
1055
|
+
const sourceAssistant = stage?.assistantMessage
|
|
1056
|
+
?? [...visible].reverse().find((message) => message.role === 'assistant');
|
|
1057
|
+
const userMessage: IFlexMessage = cloneSerializable(sourceUser ?? {
|
|
1058
|
+
messageId: plugins.crypto.randomUUID(),
|
|
1059
|
+
sessionId,
|
|
1060
|
+
runId,
|
|
1061
|
+
role: 'user',
|
|
1062
|
+
status,
|
|
1063
|
+
createdAt: timestamp,
|
|
1064
|
+
parts: [],
|
|
1065
|
+
});
|
|
1066
|
+
const assistantMessage: IFlexMessage = cloneSerializable(sourceAssistant ?? {
|
|
1067
|
+
messageId: plugins.crypto.randomUUID(),
|
|
1068
|
+
sessionId,
|
|
1069
|
+
runId,
|
|
1070
|
+
role: 'assistant',
|
|
1071
|
+
status,
|
|
1072
|
+
createdAt: timestamp,
|
|
1073
|
+
parts: [],
|
|
1074
|
+
});
|
|
1075
|
+
userMessage.status = status;
|
|
1076
|
+
assistantMessage.status = status;
|
|
1077
|
+
userMessage.completedAt ??= timestamp;
|
|
1078
|
+
assistantMessage.completedAt ??= timestamp;
|
|
1079
|
+
for (const part of assistantMessage.parts) {
|
|
1080
|
+
if (part.type === 'reasoning' && part.status === 'running') {
|
|
1081
|
+
part.status = status === 'completed' ? 'completed' : 'cancelled';
|
|
1082
|
+
part.completedAt = assistantMessage.completedAt;
|
|
1083
|
+
}
|
|
1084
|
+
if (part.type === 'tool' && part.status === 'running') {
|
|
1085
|
+
part.status = status === 'cancelled' ? 'cancelled' : 'failed';
|
|
1086
|
+
part.error = status === 'cancelled' ? repairCancellationMessage : externalErrorFallback.message;
|
|
1087
|
+
part.completedAt = assistantMessage.completedAt;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (status !== 'completed') {
|
|
1091
|
+
const message = status === 'cancelled' ? repairCancellationMessage : externalErrorFallback.message;
|
|
1092
|
+
userMessage.error = message;
|
|
1093
|
+
assistantMessage.error = message;
|
|
1094
|
+
if (stage?.usage) {
|
|
1095
|
+
assistantMessage.usage = cloneSerializable(stage.usage);
|
|
1096
|
+
} else {
|
|
1097
|
+
delete assistantMessage.usage;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return {
|
|
1101
|
+
runId,
|
|
1102
|
+
status,
|
|
1103
|
+
userMessage,
|
|
1104
|
+
assistantMessage,
|
|
1105
|
+
...(status === 'completed' && stage?.model ? { model: stage.model } : {}),
|
|
1106
|
+
...(stage?.usage ? { usage: stage.usage } : {}),
|
|
1107
|
+
...(status === 'completed' && stage?.finishReason ? { finishReason: stage.finishReason } : {}),
|
|
1108
|
+
...(status === 'completed' && stage?.steps !== undefined ? { steps: stage.steps } : {}),
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
public withScopeMutationOwnership<TValue>(
|
|
1113
|
+
state: IStorageState,
|
|
1114
|
+
operation: () => TValue | Promise<TValue>,
|
|
1115
|
+
): Promise<TValue> {
|
|
1116
|
+
const owned = state.scopeQueue.then(async (): Promise<TValue> => {
|
|
1117
|
+
this.assertStateAcceptingWork(state);
|
|
1118
|
+
return operation();
|
|
1119
|
+
});
|
|
1120
|
+
state.scopeQueue = owned.then(() => undefined, () => undefined);
|
|
1121
|
+
return owned;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
public mutateScope<TValue>(
|
|
1125
|
+
state: IStorageState,
|
|
1126
|
+
mutation: () => TValue,
|
|
1127
|
+
allowDuringDrain = false,
|
|
1128
|
+
): Promise<TValue> {
|
|
1129
|
+
const operation = state.scopeQueue.then(() => {
|
|
1130
|
+
if (!allowDuringDrain) this.assertStateAcceptingWork(state);
|
|
1131
|
+
return this.mutateScopeOwned(state, mutation);
|
|
1132
|
+
});
|
|
1133
|
+
state.scopeQueue = operation.then(() => undefined, () => undefined);
|
|
1134
|
+
return operation;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
public async mutateScopeOwned<TValue>(
|
|
1138
|
+
state: IStorageState,
|
|
1139
|
+
mutation: () => TValue,
|
|
1140
|
+
): Promise<TValue> {
|
|
1141
|
+
const beforeRevision = state.revision;
|
|
1142
|
+
const beforeSessions = new Map([...state.sessions].map(([id, stored]) => [id, {
|
|
1143
|
+
stored,
|
|
1144
|
+
metadata: cloneSerializable(stored.session),
|
|
1145
|
+
}]));
|
|
1146
|
+
const beforeTombstones = new Map([...state.tombstones].map(([id, value]) => [id, cloneSerializable(value)]));
|
|
1147
|
+
const beforeRetainedCleanups = new Map(state.retainedSessionCleanups);
|
|
1148
|
+
const beforeSnapshot: IFlexScopeSnapshot = {
|
|
1149
|
+
schemaVersion: 1,
|
|
1150
|
+
revision: beforeRevision,
|
|
1151
|
+
sessions: [...beforeSessions.values()]
|
|
1152
|
+
.map((entry) => cloneSerializable(entry.metadata))
|
|
1153
|
+
.sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
|
|
1154
|
+
tombstones: [...beforeTombstones.values()]
|
|
1155
|
+
.map((value) => cloneSerializable(value))
|
|
1156
|
+
.sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
|
|
1157
|
+
};
|
|
1158
|
+
const restore = () => {
|
|
1159
|
+
state.revision = beforeRevision;
|
|
1160
|
+
state.sessions.clear();
|
|
1161
|
+
for (const [id, entry] of beforeSessions) {
|
|
1162
|
+
entry.stored.session = entry.metadata;
|
|
1163
|
+
state.sessions.set(id, entry.stored);
|
|
1164
|
+
}
|
|
1165
|
+
state.tombstones.clear();
|
|
1166
|
+
for (const [id, value] of beforeTombstones) state.tombstones.set(id, value);
|
|
1167
|
+
state.retainedSessionCleanups.clear();
|
|
1168
|
+
for (const [id, retained] of beforeRetainedCleanups) {
|
|
1169
|
+
state.retainedSessionCleanups.set(id, retained);
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
let result: TValue;
|
|
1173
|
+
try {
|
|
1174
|
+
result = mutation();
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
restore();
|
|
1177
|
+
throw error;
|
|
1178
|
+
}
|
|
1179
|
+
const snapshot = this.createScopeSnapshot(state, beforeRevision + 1);
|
|
1180
|
+
try {
|
|
1181
|
+
await this.stores.scopes.save(state.storageKey, snapshot, beforeRevision);
|
|
1182
|
+
state.revision = snapshot.revision;
|
|
1183
|
+
return result;
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
if (error instanceof FlexHarnessStoreConflictError) {
|
|
1186
|
+
restore();
|
|
1187
|
+
throw error;
|
|
1188
|
+
}
|
|
1189
|
+
if (error instanceof FlexHarnessStoreCommitUncertainError) {
|
|
1190
|
+
state.revision = snapshot.revision;
|
|
1191
|
+
state.lifecycle = 'fenced';
|
|
1192
|
+
throw error;
|
|
1193
|
+
}
|
|
1194
|
+
let current: IFlexScopeSnapshot | undefined;
|
|
1195
|
+
let reconciliationError: unknown;
|
|
1196
|
+
try {
|
|
1197
|
+
current = await this.stores.scopes.load(state.storageKey);
|
|
1198
|
+
} catch (loadError) {
|
|
1199
|
+
reconciliationError = loadError;
|
|
1200
|
+
}
|
|
1201
|
+
if (current && JSON.stringify(current) === JSON.stringify(snapshot)) {
|
|
1202
|
+
state.revision = snapshot.revision;
|
|
1203
|
+
throw error;
|
|
1204
|
+
}
|
|
1205
|
+
if (
|
|
1206
|
+
(current === undefined && beforeRevision === 0)
|
|
1207
|
+
|| (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot))
|
|
1208
|
+
) {
|
|
1209
|
+
restore();
|
|
1210
|
+
throw error;
|
|
1211
|
+
}
|
|
1212
|
+
state.revision = snapshot.revision;
|
|
1213
|
+
state.lifecycle = 'fenced';
|
|
1214
|
+
throw reconciliationError === undefined
|
|
1215
|
+
? error
|
|
1216
|
+
: combineErrors([error, reconciliationError]);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
public mutateProjection<TValue>(
|
|
1221
|
+
state: IStorageState,
|
|
1222
|
+
stored: IStoredSessionState,
|
|
1223
|
+
mutation: () => TValue,
|
|
1224
|
+
_allowDuringDrain = false,
|
|
1225
|
+
): Promise<TValue> {
|
|
1226
|
+
const operation = stored.projectionQueue.then(async () => {
|
|
1227
|
+
const beforeRevision = stored.projectionRevision;
|
|
1228
|
+
const beforeSchemaVersion = stored.projectionSchemaVersion;
|
|
1229
|
+
const beforeReconciliationRequired = stored.projectionReconciliationRequired;
|
|
1230
|
+
const beforeBaseline = stored.projectionBaseline === undefined
|
|
1231
|
+
? undefined
|
|
1232
|
+
: cloneSerializable(stored.projectionBaseline);
|
|
1233
|
+
const beforeMetrics = cloneSerializable(stored.metrics);
|
|
1234
|
+
const beforeMessages = cloneSerializable(stored.messages);
|
|
1235
|
+
const beforeStages = cloneSerializable(stored.stagedTerminals);
|
|
1236
|
+
const beforeSegments = cloneSerializable(stored.reversionSegments);
|
|
1237
|
+
const beforeCursor = stored.revertCursor;
|
|
1238
|
+
const beforeExcludedRunIds = cloneSerializable(stored.excludedRunIds);
|
|
1239
|
+
const beforePendingReversion = stored.pendingReversion === undefined
|
|
1240
|
+
? undefined
|
|
1241
|
+
: cloneSerializable(stored.pendingReversion);
|
|
1242
|
+
const beforePendingReversionReleases = cloneSerializable(stored.pendingReversionReleases);
|
|
1243
|
+
const beforeSnapshot: TFlexProjectionSnapshot = beforeBaseline ?? {
|
|
1244
|
+
schemaVersion: 3,
|
|
1245
|
+
revision: beforeRevision,
|
|
1246
|
+
metrics: cloneSerializable(beforeMetrics),
|
|
1247
|
+
messages: cloneSerializable(beforeMessages),
|
|
1248
|
+
stagedTerminals: cloneSerializable(beforeStages),
|
|
1249
|
+
reversionSegments: cloneSerializable(beforeSegments),
|
|
1250
|
+
revertCursor: beforeCursor,
|
|
1251
|
+
excludedRunIds: cloneSerializable(beforeExcludedRunIds),
|
|
1252
|
+
...(beforePendingReversion === undefined
|
|
1253
|
+
? {}
|
|
1254
|
+
: { pendingReversion: cloneSerializable(beforePendingReversion) }),
|
|
1255
|
+
pendingReversionReleases: cloneSerializable(beforePendingReversionReleases),
|
|
1256
|
+
};
|
|
1257
|
+
const restore = () => {
|
|
1258
|
+
stored.projectionSchemaVersion = beforeSchemaVersion;
|
|
1259
|
+
stored.projectionReconciliationRequired = beforeReconciliationRequired;
|
|
1260
|
+
if (beforeBaseline === undefined) delete stored.projectionBaseline;
|
|
1261
|
+
else stored.projectionBaseline = beforeBaseline;
|
|
1262
|
+
stored.projectionRevision = beforeRevision;
|
|
1263
|
+
stored.metrics = beforeMetrics;
|
|
1264
|
+
stored.messages = beforeMessages;
|
|
1265
|
+
stored.stagedTerminals = beforeStages;
|
|
1266
|
+
stored.reversionSegments = beforeSegments;
|
|
1267
|
+
stored.revertCursor = beforeCursor;
|
|
1268
|
+
stored.excludedRunIds = beforeExcludedRunIds;
|
|
1269
|
+
if (beforePendingReversion === undefined) delete stored.pendingReversion;
|
|
1270
|
+
else stored.pendingReversion = beforePendingReversion;
|
|
1271
|
+
stored.pendingReversionReleases = beforePendingReversionReleases;
|
|
1272
|
+
};
|
|
1273
|
+
let result: TValue;
|
|
1274
|
+
try {
|
|
1275
|
+
result = mutation();
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
restore();
|
|
1278
|
+
throw error;
|
|
1279
|
+
}
|
|
1280
|
+
const snapshot = this.createProjectionSnapshot(stored, beforeRevision + 1);
|
|
1281
|
+
try {
|
|
1282
|
+
await this.stores.projections.save(
|
|
1283
|
+
state.storageKey,
|
|
1284
|
+
stored.session.sessionId,
|
|
1285
|
+
snapshot,
|
|
1286
|
+
beforeRevision,
|
|
1287
|
+
);
|
|
1288
|
+
stored.projectionSchemaVersion = 3;
|
|
1289
|
+
delete stored.projectionBaseline;
|
|
1290
|
+
stored.projectionReconciliationRequired = false;
|
|
1291
|
+
stored.projectionRevision = snapshot.revision;
|
|
1292
|
+
return result;
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
if (error instanceof FlexHarnessStoreConflictError) {
|
|
1295
|
+
restore();
|
|
1296
|
+
throw error;
|
|
1297
|
+
}
|
|
1298
|
+
if (error instanceof FlexHarnessStoreCommitUncertainError) {
|
|
1299
|
+
stored.projectionSchemaVersion = 3;
|
|
1300
|
+
stored.projectionReconciliationRequired = true;
|
|
1301
|
+
stored.projectionRevision = snapshot.revision;
|
|
1302
|
+
state.lifecycle = 'fenced';
|
|
1303
|
+
throw error;
|
|
1304
|
+
}
|
|
1305
|
+
let current: TFlexProjectionSnapshot | undefined;
|
|
1306
|
+
let reconciliationError: unknown;
|
|
1307
|
+
try {
|
|
1308
|
+
current = await this.stores.projections.load(
|
|
1309
|
+
state.storageKey,
|
|
1310
|
+
stored.session.sessionId,
|
|
1311
|
+
);
|
|
1312
|
+
} catch (loadError) {
|
|
1313
|
+
reconciliationError = loadError;
|
|
1314
|
+
}
|
|
1315
|
+
if (current && JSON.stringify(current) === JSON.stringify(snapshot)) {
|
|
1316
|
+
stored.projectionSchemaVersion = 3;
|
|
1317
|
+
delete stored.projectionBaseline;
|
|
1318
|
+
stored.projectionReconciliationRequired = false;
|
|
1319
|
+
stored.projectionRevision = snapshot.revision;
|
|
1320
|
+
throw error;
|
|
1321
|
+
}
|
|
1322
|
+
if (
|
|
1323
|
+
(current === undefined && beforeRevision === 0)
|
|
1324
|
+
|| (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot))
|
|
1325
|
+
) {
|
|
1326
|
+
restore();
|
|
1327
|
+
throw error;
|
|
1328
|
+
}
|
|
1329
|
+
stored.projectionSchemaVersion = 3;
|
|
1330
|
+
stored.projectionReconciliationRequired = true;
|
|
1331
|
+
stored.projectionRevision = snapshot.revision;
|
|
1332
|
+
state.lifecycle = 'fenced';
|
|
1333
|
+
throw reconciliationError === undefined
|
|
1334
|
+
? error
|
|
1335
|
+
: combineErrors([error, reconciliationError]);
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
stored.projectionQueue = operation.then(() => undefined, () => undefined);
|
|
1339
|
+
return operation;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
private createScopeSnapshot(state: IStorageState, revision: number): IFlexScopeSnapshot {
|
|
1343
|
+
return {
|
|
1344
|
+
schemaVersion: 1,
|
|
1345
|
+
revision,
|
|
1346
|
+
sessions: [...state.sessions.values()]
|
|
1347
|
+
.map((stored) => cloneSerializable(stored.session))
|
|
1348
|
+
.sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
|
|
1349
|
+
tombstones: [...state.tombstones.values()]
|
|
1350
|
+
.map((value) => cloneSerializable(value))
|
|
1351
|
+
.sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
private createProjectionSnapshot(
|
|
1356
|
+
stored: IStoredSessionState,
|
|
1357
|
+
revision: number,
|
|
1358
|
+
): IFlexProjectionSnapshotCurrent {
|
|
1359
|
+
return {
|
|
1360
|
+
schemaVersion: 3,
|
|
1361
|
+
revision,
|
|
1362
|
+
metrics: cloneSerializable(stored.metrics),
|
|
1363
|
+
messages: cloneSerializable(stored.messages),
|
|
1364
|
+
stagedTerminals: cloneSerializable(stored.stagedTerminals),
|
|
1365
|
+
reversionSegments: cloneSerializable(stored.reversionSegments),
|
|
1366
|
+
revertCursor: stored.revertCursor,
|
|
1367
|
+
excludedRunIds: cloneSerializable(stored.excludedRunIds),
|
|
1368
|
+
...(stored.pendingReversion === undefined
|
|
1369
|
+
? {}
|
|
1370
|
+
: { pendingReversion: cloneSerializable(stored.pendingReversion) }),
|
|
1371
|
+
pendingReversionReleases: cloneSerializable(stored.pendingReversionReleases),
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
public upgradeProjectionSnapshot(
|
|
1376
|
+
projection: TFlexProjectionSnapshot,
|
|
1377
|
+
): IFlexProjectionSnapshotCurrent {
|
|
1378
|
+
if (projection.schemaVersion === 3) return cloneSerializable(projection);
|
|
1379
|
+
if (projection.schemaVersion === 1) {
|
|
1380
|
+
return {
|
|
1381
|
+
schemaVersion: 3,
|
|
1382
|
+
revision: projection.revision,
|
|
1383
|
+
messages: cloneSerializable(projection.messages),
|
|
1384
|
+
stagedTerminals: cloneSerializable(projection.stagedTerminals),
|
|
1385
|
+
reversionSegments: [],
|
|
1386
|
+
revertCursor: 0,
|
|
1387
|
+
excludedRunIds: [],
|
|
1388
|
+
pendingReversionReleases: [],
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
return {
|
|
1392
|
+
schemaVersion: 3,
|
|
1393
|
+
revision: projection.revision,
|
|
1394
|
+
messages: cloneSerializable(projection.messages),
|
|
1395
|
+
stagedTerminals: cloneSerializable(projection.stagedTerminals),
|
|
1396
|
+
reversionSegments: projection.reversionSegments.map((segment) => ({
|
|
1397
|
+
...cloneSerializable(segment),
|
|
1398
|
+
protocolVersion: 1,
|
|
1399
|
+
provenance: segment.workspaceCaptured ? 'workspace' as const : 'transcript' as const,
|
|
1400
|
+
...(segment.workspaceCaptured
|
|
1401
|
+
? { disposition: segment.status === 'capturing' ? 'pending' as const : 'revertible' as const }
|
|
1402
|
+
: {}),
|
|
1403
|
+
})),
|
|
1404
|
+
revertCursor: projection.revertCursor,
|
|
1405
|
+
excludedRunIds: cloneSerializable(projection.excludedRunIds),
|
|
1406
|
+
...(projection.pendingReversion === undefined
|
|
1407
|
+
? {}
|
|
1408
|
+
: {
|
|
1409
|
+
pendingReversion: projection.pendingReversion.kind === 'capture'
|
|
1410
|
+
? { ...cloneSerializable(projection.pendingReversion), protocolVersion: 1 }
|
|
1411
|
+
: cloneSerializable(projection.pendingReversion),
|
|
1412
|
+
}),
|
|
1413
|
+
pendingReversionReleases: projection.pendingReversionReleases.map((release) => ({
|
|
1414
|
+
...cloneSerializable(release),
|
|
1415
|
+
protocolVersion: 1,
|
|
1416
|
+
})),
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
public async reconcileProjectionFromStore(stored: IStoredSessionState): Promise<void> {
|
|
1421
|
+
if (!stored.projectionReconciliationRequired) return;
|
|
1422
|
+
const projection = await this.stores.projections.load(
|
|
1423
|
+
stored.storageKey,
|
|
1424
|
+
stored.session.sessionId,
|
|
1425
|
+
);
|
|
1426
|
+
if (!projection) {
|
|
1427
|
+
throw new FlexHarnessValidationError('Uncertain projection persistence could not be reloaded.');
|
|
1428
|
+
}
|
|
1429
|
+
const current = this.upgradeProjectionSnapshot(projection);
|
|
1430
|
+
stored.messages = current.messages;
|
|
1431
|
+
stored.stagedTerminals = current.stagedTerminals;
|
|
1432
|
+
stored.reversionSegments = current.reversionSegments;
|
|
1433
|
+
stored.revertCursor = current.revertCursor;
|
|
1434
|
+
stored.excludedRunIds = current.excludedRunIds;
|
|
1435
|
+
if (current.pendingReversion === undefined) delete stored.pendingReversion;
|
|
1436
|
+
else stored.pendingReversion = current.pendingReversion;
|
|
1437
|
+
stored.pendingReversionReleases = current.pendingReversionReleases;
|
|
1438
|
+
if (projection.schemaVersion === 3) delete stored.projectionBaseline;
|
|
1439
|
+
else stored.projectionBaseline = cloneSerializable(projection);
|
|
1440
|
+
stored.projectionSchemaVersion = projection.schemaVersion;
|
|
1441
|
+
stored.projectionRevision = projection.revision;
|
|
1442
|
+
stored.projectionReconciliationRequired = false;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
public sessionsAreDependencyRelated(
|
|
1446
|
+
state: IStorageState,
|
|
1447
|
+
leftSessionId: string,
|
|
1448
|
+
rightSessionId: string,
|
|
1449
|
+
): boolean {
|
|
1450
|
+
return this.isSessionAncestor(state, leftSessionId, rightSessionId)
|
|
1451
|
+
|| this.isSessionAncestor(state, rightSessionId, leftSessionId);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
public isSessionAncestor(
|
|
1455
|
+
state: IStorageState,
|
|
1456
|
+
ancestorSessionId: string,
|
|
1457
|
+
descendantSessionId: string,
|
|
1458
|
+
): boolean {
|
|
1459
|
+
const visited = new Set<string>();
|
|
1460
|
+
let currentId: string | undefined = descendantSessionId;
|
|
1461
|
+
while (currentId) {
|
|
1462
|
+
if (visited.has(currentId)) return false;
|
|
1463
|
+
visited.add(currentId);
|
|
1464
|
+
const parentSessionId = this.sessionParentSessionId(state, currentId);
|
|
1465
|
+
if (parentSessionId === ancestorSessionId) return true;
|
|
1466
|
+
currentId = parentSessionId;
|
|
1467
|
+
}
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
public sessionAncestryDepth(state: IStorageState, sessionId: string): number {
|
|
1472
|
+
const visited = new Set<string>();
|
|
1473
|
+
let currentId: string | undefined = sessionId;
|
|
1474
|
+
let depth = 0;
|
|
1475
|
+
while (currentId) {
|
|
1476
|
+
if (visited.has(currentId)) return depth;
|
|
1477
|
+
visited.add(currentId);
|
|
1478
|
+
const parentSessionId = this.sessionParentSessionId(state, currentId);
|
|
1479
|
+
if (!parentSessionId) return depth;
|
|
1480
|
+
depth++;
|
|
1481
|
+
currentId = parentSessionId;
|
|
1482
|
+
}
|
|
1483
|
+
return depth;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
private sessionParentSessionId(state: IStorageState, sessionId: string): string | undefined {
|
|
1487
|
+
return this.sessionMetadata(state, sessionId)?.parentSessionId
|
|
1488
|
+
?? state.tombstones.get(sessionId)?.parentSessionId;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
private sessionMetadata(state: IStorageState, sessionId: string): IFlexSession | undefined {
|
|
1492
|
+
return state.sessions.get(sessionId)?.session
|
|
1493
|
+
?? state.retainedSessionCleanups.get(sessionId)?.stored.session;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
public appendUnexpectedErrors(target: unknown[], error: unknown): void {
|
|
1497
|
+
if (error instanceof FlexHarnessRunError) {
|
|
1498
|
+
for (const nested of error.errors) this.appendUnexpectedErrors(target, nested);
|
|
1499
|
+
} else if (!isAbortError(error)) target.push(error);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
public async resolveState(
|
|
1503
|
+
scopeId: string,
|
|
1504
|
+
): Promise<{ scope: IFlexResolvedScope<TScope>; state: IStorageState }> {
|
|
1505
|
+
this.assertOpen();
|
|
1506
|
+
validateIdentifier(scopeId, 'scopeId');
|
|
1507
|
+
const admission = this.getScopeAdmission(scopeId);
|
|
1508
|
+
if (admission.retiring) throw this.createScopeRetirementError();
|
|
1509
|
+
const generation = admission.generation;
|
|
1510
|
+
admission.inFlightResolvers++;
|
|
1511
|
+
try {
|
|
1512
|
+
const scope = await this.scopeResolver.resolveScope(scopeId);
|
|
1513
|
+
this.assertOpen();
|
|
1514
|
+
if (admission.retiring || admission.generation !== generation) throw this.createScopeRetirementError();
|
|
1515
|
+
validateIdentifier(scope.storageKey, 'resolved storageKey');
|
|
1516
|
+
if (this.lifecycle.storageDrains.has(scope.storageKey)) throw this.createScopeRetirementError();
|
|
1517
|
+
let stateLoad = this.stateLoads.get(scope.storageKey);
|
|
1518
|
+
if (!stateLoad) {
|
|
1519
|
+
const orphanErrors = await this.lifecycle.closeOrphanedResources(scope.storageKey);
|
|
1520
|
+
if (orphanErrors.length > 0) throw combineErrors(orphanErrors);
|
|
1521
|
+
this.assertOpen();
|
|
1522
|
+
if (
|
|
1523
|
+
admission.retiring
|
|
1524
|
+
|| admission.generation !== generation
|
|
1525
|
+
|| this.lifecycle.storageDrains.has(scope.storageKey)
|
|
1526
|
+
) throw this.createScopeRetirementError();
|
|
1527
|
+
stateLoad = this.stateLoads.get(scope.storageKey);
|
|
1528
|
+
if (!stateLoad) {
|
|
1529
|
+
const compactorLifecycleController = new AbortController();
|
|
1530
|
+
this.lifecycle.storageCompactorLifecycleControllers.set(
|
|
1531
|
+
scope.storageKey,
|
|
1532
|
+
compactorLifecycleController,
|
|
1533
|
+
);
|
|
1534
|
+
stateLoad = this.loadState(
|
|
1535
|
+
scope.storageKey,
|
|
1536
|
+
scopeId,
|
|
1537
|
+
scope.scope,
|
|
1538
|
+
compactorLifecycleController,
|
|
1539
|
+
);
|
|
1540
|
+
this.stateLoads.set(scope.storageKey, stateLoad);
|
|
1541
|
+
void stateLoad.catch(() => {
|
|
1542
|
+
if (this.stateLoads.get(scope.storageKey) === stateLoad) {
|
|
1543
|
+
this.stateLoads.delete(scope.storageKey);
|
|
1544
|
+
if (
|
|
1545
|
+
this.lifecycle.storageCompactorLifecycleControllers.get(scope.storageKey)
|
|
1546
|
+
=== compactorLifecycleController
|
|
1547
|
+
) this.lifecycle.storageCompactorLifecycleControllers.delete(scope.storageKey);
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
const state = await stateLoad;
|
|
1553
|
+
this.assertOpen();
|
|
1554
|
+
if (state.lifecycle === 'fenced') {
|
|
1555
|
+
const drain = this.lifecycle.storageDrains.get(scope.storageKey) ?? this.lifecycle.drainStorage(
|
|
1556
|
+
scope.storageKey,
|
|
1557
|
+
stateLoad,
|
|
1558
|
+
this.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.')),
|
|
1559
|
+
{ scopeId, scope: scope.scope },
|
|
1560
|
+
);
|
|
1561
|
+
if (this.commands.ambientSlashCommandOwner({ storageKey: scope.storageKey })) {
|
|
1562
|
+
throw this.createScopeRetirementError();
|
|
1563
|
+
}
|
|
1564
|
+
try {
|
|
1565
|
+
await drain;
|
|
1566
|
+
} catch {
|
|
1567
|
+
// Fenced cleanup ownership remains available to explicit retirement or disposal.
|
|
1568
|
+
}
|
|
1569
|
+
throw this.createScopeRetirementError();
|
|
1570
|
+
}
|
|
1571
|
+
if (
|
|
1572
|
+
admission.retiring
|
|
1573
|
+
|| admission.generation !== generation
|
|
1574
|
+
|| this.lifecycle.storageDrains.has(scope.storageKey)
|
|
1575
|
+
|| state.lifecycle !== 'active'
|
|
1576
|
+
) throw this.createScopeRetirementError();
|
|
1577
|
+
return { scope, state };
|
|
1578
|
+
} finally {
|
|
1579
|
+
admission.inFlightResolvers--;
|
|
1580
|
+
this.pruneScopeAdmission(scopeId, admission);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
public getScopeAdmission(scopeId: string): IScopeAdmissionState {
|
|
1585
|
+
let admission = this.scopeAdmissions.get(scopeId);
|
|
1586
|
+
if (!admission) {
|
|
1587
|
+
admission = { generation: 0, inFlightResolvers: 0, retiring: false };
|
|
1588
|
+
this.scopeAdmissions.set(scopeId, admission);
|
|
1589
|
+
}
|
|
1590
|
+
return admission;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
public pruneScopeAdmission(scopeId: string, admission: IScopeAdmissionState): void {
|
|
1594
|
+
if (
|
|
1595
|
+
!admission.retiring
|
|
1596
|
+
&& admission.inFlightResolvers === 0
|
|
1597
|
+
&& this.scopeAdmissions.get(scopeId) === admission
|
|
1598
|
+
) this.scopeAdmissions.delete(scopeId);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
public createScopeRetirementError(): FlexHarnessAbortError {
|
|
1602
|
+
return this.trustInternalError(new FlexHarnessAbortError(scopeRetirementMessage));
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
public assertStateAcceptingWork(state: IStorageState): void {
|
|
1606
|
+
if (state.lifecycle !== 'active' || this.lifecycle.storageDrains.has(state.storageKey)) {
|
|
1607
|
+
throw this.createScopeRetirementError();
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
public requireSession(state: IStorageState, sessionId: string): IStoredSessionState {
|
|
1612
|
+
validateIdentifier(sessionId, 'sessionId');
|
|
1613
|
+
requireTransferIdentifier(sessionId, 'sessionId');
|
|
1614
|
+
if (state.tombstones.has(sessionId)) throw new FlexHarnessNotFoundError('Session', sessionId);
|
|
1615
|
+
if (state.initializingSessions.has(sessionId)) {
|
|
1616
|
+
throw new FlexHarnessSessionBusyError(sessionId, 'is still being initialized');
|
|
1617
|
+
}
|
|
1618
|
+
const stored = state.sessions.get(sessionId);
|
|
1619
|
+
if (!stored) throw new FlexHarnessNotFoundError('Session', sessionId);
|
|
1620
|
+
return stored;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
public visibleMessages(stored: IStoredSessionState): IFlexMessage[] {
|
|
1624
|
+
const firstHiddenSegment = this.reversions.reversionGroups(stored)[stored.revertCursor]?.segments[0];
|
|
1625
|
+
if (!firstHiddenSegment) return stored.messages;
|
|
1626
|
+
const boundary = stored.messages.findIndex((message) => message.runId === firstHiddenSegment.runId);
|
|
1627
|
+
return boundary < 0 ? stored.messages : stored.messages.slice(0, boundary);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
public requireMutableSession(state: IStorageState, sessionId: string): IStoredSessionState {
|
|
1631
|
+
const stored = this.requireSession(state, sessionId);
|
|
1632
|
+
const pending = [...state.pendingPermissions.values()].some((entry) =>
|
|
1633
|
+
!entry.settled && entry.request.sessionId === sessionId);
|
|
1634
|
+
if (stored.outstandingPromptsById.size > 0 || pending) {
|
|
1635
|
+
throw new FlexHarnessSessionBusyError(
|
|
1636
|
+
sessionId,
|
|
1637
|
+
'cannot be changed while it has an outstanding prompt or pending permission',
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
return stored;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/** Refuses an operation that would change an imported session's history or model context. */
|
|
1644
|
+
public assertSessionWritable(stored: IStoredSessionState): void {
|
|
1645
|
+
if (stored.session.origin !== undefined) {
|
|
1646
|
+
throw this.trustInternalError(new FlexHarnessImportedSessionError(stored.session.sessionId));
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/** An imported session never ran here, so it has no hidden stage and no reversion state. */
|
|
1651
|
+
private assertImportedSessionState(stored: IStoredSessionState): void {
|
|
1652
|
+
if (
|
|
1653
|
+
stored.stagedTerminals.length > 0
|
|
1654
|
+
|| stored.reversionSegments.length > 0
|
|
1655
|
+
|| stored.excludedRunIds.length > 0
|
|
1656
|
+
|| stored.pendingReversion !== undefined
|
|
1657
|
+
|| stored.pendingReversionReleases.length > 0
|
|
1658
|
+
|| stored.messages.some((message) => message.status === 'streaming')
|
|
1659
|
+
) {
|
|
1660
|
+
throw new FlexHarnessValidationError(
|
|
1661
|
+
`Imported session "${stored.session.sessionId}" has run state it cannot have.`,
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
public requireMessage(stored: IStoredSessionState, messageId: string): IFlexMessage {
|
|
1667
|
+
const message = this.visibleMessages(stored).find((entry) => entry.messageId === messageId);
|
|
1668
|
+
if (!message) throw new FlexHarnessNotFoundError('Message', messageId);
|
|
1669
|
+
return message;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
public isTombstoned(run: IActiveRun): boolean {
|
|
1673
|
+
return run.state.tombstones.has(run.sessionId) || run.state.sessions.get(run.sessionId) !== run.stored;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
public emitPartEvent(
|
|
1677
|
+
run: IActiveRun,
|
|
1678
|
+
type: IFlexPartSnapshotEvent['type'],
|
|
1679
|
+
part: TFlexMessagePart,
|
|
1680
|
+
): void {
|
|
1681
|
+
const { messageIndex, partIndex } = this.partEventCoordinates(run, part.partId);
|
|
1682
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
1683
|
+
type,
|
|
1684
|
+
runId: run.runId,
|
|
1685
|
+
messageId: run.assistantMessageId,
|
|
1686
|
+
messageIndex,
|
|
1687
|
+
partId: part.partId,
|
|
1688
|
+
partIndex,
|
|
1689
|
+
part: publicSnapshot(part),
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
public emitPartDeltaEvent(
|
|
1694
|
+
run: IActiveRun,
|
|
1695
|
+
part: Extract<TFlexMessagePart, { type: 'text' | 'reasoning' }>,
|
|
1696
|
+
delta: string,
|
|
1697
|
+
accounting: { baseTextUtf8Bytes: number; textUtf8Bytes: number },
|
|
1698
|
+
): void {
|
|
1699
|
+
const { messageIndex, partIndex } = this.partEventCoordinates(run, part.partId);
|
|
1700
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
1701
|
+
type: 'part.delta',
|
|
1702
|
+
runId: run.runId,
|
|
1703
|
+
messageId: run.assistantMessageId,
|
|
1704
|
+
messageIndex,
|
|
1705
|
+
partId: part.partId,
|
|
1706
|
+
partIndex,
|
|
1707
|
+
partType: part.type,
|
|
1708
|
+
delta,
|
|
1709
|
+
...accounting,
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
private partEventCoordinates(
|
|
1714
|
+
run: IActiveRun,
|
|
1715
|
+
partId: string,
|
|
1716
|
+
): { messageIndex: number; partIndex: number } {
|
|
1717
|
+
const messageIndex = run.stored.messages.findIndex(
|
|
1718
|
+
(message) => message.messageId === run.assistantMessageId,
|
|
1719
|
+
);
|
|
1720
|
+
const partIndex = run.callbackParts.findIndex((entry) => entry.partId === partId);
|
|
1721
|
+
if (
|
|
1722
|
+
messageIndex < 0
|
|
1723
|
+
|| run.stored.messages[messageIndex]?.messageId !== run.assistantMessageId
|
|
1724
|
+
|| partIndex < 0
|
|
1725
|
+
|| run.callbackParts[partIndex]?.partId !== partId
|
|
1726
|
+
) throw new Error('Flex part event source coordinates are unavailable.');
|
|
1727
|
+
return { messageIndex, partIndex };
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
public emitEvent(scopeId: string, sourceSession: IFlexSession, details: TEventDetails): void {
|
|
1731
|
+
const generation = requireSessionGeneration(sourceSession);
|
|
1732
|
+
const event = deepFreeze(cloneSerializable({
|
|
1733
|
+
eventId: plugins.crypto.randomUUID(),
|
|
1734
|
+
sequence: ++this.sequence,
|
|
1735
|
+
timestamp: new Date().toISOString(),
|
|
1736
|
+
scopeId,
|
|
1737
|
+
...details,
|
|
1738
|
+
sessionId: sourceSession.sessionId,
|
|
1739
|
+
...generation,
|
|
1740
|
+
})) as TFlexHarnessEvent;
|
|
1741
|
+
for (const listener of this.listeners) {
|
|
1742
|
+
try {
|
|
1743
|
+
listener(event);
|
|
1744
|
+
} catch {
|
|
1745
|
+
// Public listener failures are isolated from persistence and run ownership.
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
public validateRuntimeEvent(event: unknown): asserts event is IJsonObject & { type: string } {
|
|
1751
|
+
try {
|
|
1752
|
+
assertJsonSerializable(event, '$event');
|
|
1753
|
+
} catch {
|
|
1754
|
+
throw new FlexHarnessValidationError('Runtime event must be JSON-serializable.');
|
|
1755
|
+
}
|
|
1756
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) {
|
|
1757
|
+
throw new FlexHarnessValidationError('Runtime event must be a plain JSON object.');
|
|
1758
|
+
}
|
|
1759
|
+
validateIdentifier((event as { type?: string }).type!, 'runtime event type');
|
|
1760
|
+
if (jsonBytes(event) > this.toolOutputLimits.maxBytes) {
|
|
1761
|
+
throw new FlexHarnessValidationError('Runtime event exceeds the byte limit.');
|
|
1762
|
+
}
|
|
1763
|
+
const visit = (value: unknown, depth: number): void => {
|
|
1764
|
+
if (!value || typeof value !== 'object') return;
|
|
1765
|
+
if (depth > this.toolOutputLimits.maxDepth) {
|
|
1766
|
+
throw new FlexHarnessValidationError('Runtime event exceeds the depth limit.');
|
|
1767
|
+
}
|
|
1768
|
+
for (const entry of Array.isArray(value) ? value : Object.values(value)) visit(entry, depth + 1);
|
|
1769
|
+
};
|
|
1770
|
+
visit(event, 0);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
public boundedIdentifier(value: string, name: string): string {
|
|
1774
|
+
validateIdentifier(value, name);
|
|
1775
|
+
requireTransferIdentifier(value, name);
|
|
1776
|
+
return value;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
public messageCursorNamespace(storageKey: string): string {
|
|
1780
|
+
return plugins.crypto.createHash('sha256').update(storageKey).digest('base64url');
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
public createMessageCursor(namespace: string, sessionId: string, anchorMessageId: string): string {
|
|
1784
|
+
const cursor = Buffer.from(JSON.stringify({
|
|
1785
|
+
version: 1,
|
|
1786
|
+
namespace,
|
|
1787
|
+
sessionId,
|
|
1788
|
+
anchorMessageId,
|
|
1789
|
+
} satisfies IFlexMessageCursor)).toString('base64url');
|
|
1790
|
+
if (Buffer.byteLength(cursor, 'utf8') > maxMessagePageCursorBytes) {
|
|
1791
|
+
throw new FlexHarnessValidationError('Message page cursor exceeds its transfer limit.');
|
|
1792
|
+
}
|
|
1793
|
+
return cursor;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
public parseMessageCursor(cursor: string): IFlexMessageCursor {
|
|
1797
|
+
let parsed: unknown;
|
|
1798
|
+
try {
|
|
1799
|
+
const decoded = Buffer.from(cursor, 'base64url');
|
|
1800
|
+
if (decoded.toString('base64url') !== cursor) throw new Error('non-canonical cursor');
|
|
1801
|
+
parsed = JSON.parse(decoded.toString('utf8'));
|
|
1802
|
+
} catch {
|
|
1803
|
+
throw new FlexHarnessValidationError('Message page cursor is invalid.');
|
|
1804
|
+
}
|
|
1805
|
+
if (
|
|
1806
|
+
!parsed
|
|
1807
|
+
|| typeof parsed !== 'object'
|
|
1808
|
+
|| Array.isArray(parsed)
|
|
1809
|
+
|| Object.keys(parsed).length !== 4
|
|
1810
|
+
|| (parsed as Partial<IFlexMessageCursor>).version !== 1
|
|
1811
|
+
|| typeof (parsed as Partial<IFlexMessageCursor>).namespace !== 'string'
|
|
1812
|
+
|| typeof (parsed as Partial<IFlexMessageCursor>).sessionId !== 'string'
|
|
1813
|
+
|| typeof (parsed as Partial<IFlexMessageCursor>).anchorMessageId !== 'string'
|
|
1814
|
+
) throw new FlexHarnessValidationError('Message page cursor is invalid.');
|
|
1815
|
+
const result = parsed as IFlexMessageCursor;
|
|
1816
|
+
if (
|
|
1817
|
+
!result.namespace
|
|
1818
|
+
|| !result.sessionId
|
|
1819
|
+
|| !result.anchorMessageId
|
|
1820
|
+
|| Buffer.byteLength(result.namespace, 'utf8') > 128
|
|
1821
|
+
|| Buffer.byteLength(result.sessionId, 'utf8') > maxTransferIdentifierBytes
|
|
1822
|
+
|| Buffer.byteLength(result.anchorMessageId, 'utf8') > maxTransferIdentifierBytes
|
|
1823
|
+
) throw new FlexHarnessValidationError('Message page cursor is invalid.');
|
|
1824
|
+
return result;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
public assertOpen(): void {
|
|
1828
|
+
if (this.closed) throw new FlexHarnessClosedError();
|
|
1829
|
+
}
|
|
1830
|
+
}
|