@myagentroam/agent 0.9.86 → 0.9.87
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.
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import type { MarAgentModelConfiguration, MarAgentReasoningEffort, ModelCredentialAcquireReason } from './configuration.js';
|
|
2
|
-
export interface ModelContinuationCheckpoint {
|
|
3
|
-
readonly version: 2;
|
|
4
|
-
readonly protocol: 'OPENAI_RESPONSES';
|
|
5
|
-
readonly configurationHash: string;
|
|
6
|
-
readonly responseId: string;
|
|
7
|
-
readonly inputPrefixLength: number;
|
|
8
|
-
}
|
|
9
2
|
export interface ClientToolDefinition {
|
|
10
3
|
name: string;
|
|
11
4
|
description: string;
|
|
@@ -75,10 +68,6 @@ export interface ModelRequest {
|
|
|
75
68
|
mimeType: string;
|
|
76
69
|
dataBase64: string;
|
|
77
70
|
}[];
|
|
78
|
-
continuation?: {
|
|
79
|
-
previousResponseId: string;
|
|
80
|
-
deltaMessages: readonly ModelMessage[];
|
|
81
|
-
};
|
|
82
71
|
onAttemptDiagnostic?: (diagnostic: ModelAttemptDiagnostic) => Promise<void> | void;
|
|
83
72
|
}
|
|
84
73
|
export interface ModelUsage {
|
|
@@ -158,13 +147,11 @@ export interface ModelAdapter {
|
|
|
158
147
|
* The returned commit callback must be synchronous and non-throwing.
|
|
159
148
|
*/
|
|
160
149
|
prepareReconfigure?(configuration: MarAgentModelConfiguration): (() => void) | undefined;
|
|
161
|
-
restoreContinuation?(checkpoint: ModelContinuationCheckpoint): boolean;
|
|
162
150
|
close?(): Promise<void>;
|
|
163
151
|
}
|
|
164
152
|
export interface ModelTurnSession {
|
|
165
153
|
start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
|
|
166
154
|
/** Drops incremental response state after a local context replacement. */
|
|
167
155
|
resetContinuation?(): void;
|
|
168
|
-
continuationCheckpoint?(): ModelContinuationCheckpoint | undefined;
|
|
169
156
|
close(): Promise<void>;
|
|
170
157
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MarAgentError } from '../error.js';
|
|
2
2
|
import { type MarAgentModelConfiguration, type ModelCredentialAcquireReason } from './configuration.js';
|
|
3
3
|
import WebSocket from 'ws';
|
|
4
|
-
import type { ModelAdapter, ModelAttemptDiagnostic,
|
|
4
|
+
import type { ModelAdapter, ModelAttemptDiagnostic, ModelEvent, ModelRequest, ModelTurnSession } from './contracts.js';
|
|
5
5
|
export declare const OPENAI_RESPONSES_EARLY_EOF_REASON = "OPENAI_RESPONSES_EARLY_EOF";
|
|
6
6
|
export declare const OPENAI_RESPONSES_WEBSOCKET_CONNECTION_LIMIT_REASON = "OPENAI_RESPONSES_WEBSOCKET_CONNECTION_LIMIT";
|
|
7
7
|
type ResponsesRequestBody = Readonly<Record<string, unknown>> & {
|
|
@@ -19,7 +19,6 @@ interface ResponsesSessionState {
|
|
|
19
19
|
readonly responseId: string;
|
|
20
20
|
readonly outputItems: readonly Record<string, unknown>[];
|
|
21
21
|
};
|
|
22
|
-
restoredContinuation?: ModelContinuationCheckpoint;
|
|
23
22
|
}
|
|
24
23
|
interface PreparedResponsesRequest {
|
|
25
24
|
readonly fullBody: ResponsesRequestBody;
|
|
@@ -32,7 +31,6 @@ export declare class OpenAiResponsesAdapter implements ModelAdapter {
|
|
|
32
31
|
constructor(configuration: MarAgentModelConfiguration);
|
|
33
32
|
get configuration(): MarAgentModelConfiguration;
|
|
34
33
|
prepareReconfigure(configuration: MarAgentModelConfiguration): (() => void) | undefined;
|
|
35
|
-
restoreContinuation(checkpoint: ModelContinuationCheckpoint): boolean;
|
|
36
34
|
start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
|
|
37
35
|
createTurnSession(): ModelTurnSession;
|
|
38
36
|
close(): Promise<void>;
|
|
@@ -49,7 +47,6 @@ declare class OpenAiResponsesTurnSession implements ModelTurnSession {
|
|
|
49
47
|
start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
|
|
50
48
|
close(): Promise<void>;
|
|
51
49
|
resetContinuation(): void;
|
|
52
|
-
continuationCheckpoint(): ModelContinuationCheckpoint | undefined;
|
|
53
50
|
prepareRequest(request: ModelRequest): PreparedResponsesRequest;
|
|
54
51
|
fullRequestBody(): ResponsesRequestBody;
|
|
55
52
|
currentTurnState(): string | undefined;
|
|
@@ -31,23 +31,6 @@ export class OpenAiResponsesAdapter {
|
|
|
31
31
|
this.#configuration = configuration;
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
restoreContinuation(checkpoint) {
|
|
35
|
-
if (this.#cacheLeased ||
|
|
36
|
-
this.#cachedState.lastRequest !== undefined ||
|
|
37
|
-
this.#cachedState.lastResponse !== undefined ||
|
|
38
|
-
!responsesPersistedContinuationEnabled(this.#configuration) ||
|
|
39
|
-
checkpoint.version !== 2 ||
|
|
40
|
-
checkpoint.protocol !== 'OPENAI_RESPONSES' ||
|
|
41
|
-
typeof checkpoint.responseId !== 'string' ||
|
|
42
|
-
checkpoint.responseId.length === 0 ||
|
|
43
|
-
checkpoint.responseId.length > 1024 ||
|
|
44
|
-
!Number.isSafeInteger(checkpoint.inputPrefixLength) ||
|
|
45
|
-
checkpoint.inputPrefixLength < 0 ||
|
|
46
|
-
checkpoint.configurationHash !== responsesContinuationIdentityHash(this.#configuration))
|
|
47
|
-
return false;
|
|
48
|
-
this.#cachedState.restoredContinuation = checkpoint;
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
34
|
async *start(request, signal) {
|
|
52
35
|
const turn = new OpenAiResponsesTurnSession(this, {}, false);
|
|
53
36
|
try {
|
|
@@ -79,8 +62,10 @@ export class OpenAiResponsesAdapter {
|
|
|
79
62
|
invalidateResponsesWebSocket(state, true);
|
|
80
63
|
return;
|
|
81
64
|
}
|
|
82
|
-
if (state.socket !== undefined && state.socket.readyState !== WebSocket.OPEN)
|
|
65
|
+
if (state.socket !== undefined && state.socket.readyState !== WebSocket.OPEN) {
|
|
83
66
|
invalidateResponsesWebSocket(state, false);
|
|
67
|
+
clearResponsesContinuation(state);
|
|
68
|
+
}
|
|
84
69
|
this.#cachedState = state;
|
|
85
70
|
this.#cacheLeased = false;
|
|
86
71
|
}
|
|
@@ -160,6 +145,24 @@ export class OpenAiResponsesAdapter {
|
|
|
160
145
|
...(options.retryDelayMs === undefined ? {} : { retryDelayMs: options.retryDelayMs }),
|
|
161
146
|
...(options.retryReason === undefined ? {} : { retryReason: options.retryReason })
|
|
162
147
|
});
|
|
148
|
+
if (!signal.aborted &&
|
|
149
|
+
!sawSemanticEvent &&
|
|
150
|
+
useContinuation &&
|
|
151
|
+
!invalidContinuationFallbackUsed &&
|
|
152
|
+
isLocalContinuationReset(error)) {
|
|
153
|
+
invalidContinuationFallbackUsed = true;
|
|
154
|
+
useContinuation = false;
|
|
155
|
+
requestBody = prepared.fullBody;
|
|
156
|
+
turn.resetContinuation();
|
|
157
|
+
turn.invalidateWebSocket();
|
|
158
|
+
retryState.attempts = Math.max(0, retryState.attempts - 1);
|
|
159
|
+
credentialReason = 'RECONNECT';
|
|
160
|
+
await reportFailure(true, {
|
|
161
|
+
retryDelayMs: 0,
|
|
162
|
+
retryReason: 'INVALID_CONTINUATION'
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
163
166
|
if (!signal.aborted &&
|
|
164
167
|
attemptNumber < maxAttempts &&
|
|
165
168
|
!sawSemanticEvent &&
|
|
@@ -169,6 +172,7 @@ export class OpenAiResponsesAdapter {
|
|
|
169
172
|
invalidContinuationFallbackUsed = true;
|
|
170
173
|
useContinuation = false;
|
|
171
174
|
requestBody = prepared.fullBody;
|
|
175
|
+
turn.resetContinuation();
|
|
172
176
|
turn.invalidateWebSocket();
|
|
173
177
|
credentialReason = 'RECONNECT';
|
|
174
178
|
await reportFailure(true, {
|
|
@@ -637,7 +641,6 @@ class OpenAiResponsesTurnSession {
|
|
|
637
641
|
(outputItems.length > 0 || !sawUnrepresentedOutput)) {
|
|
638
642
|
this.state.lastRequest = prepared.fullBody;
|
|
639
643
|
this.state.lastResponse = { responseId, outputItems };
|
|
640
|
-
delete this.state.restoredContinuation;
|
|
641
644
|
}
|
|
642
645
|
else
|
|
643
646
|
clearResponsesContinuation(this.state);
|
|
@@ -660,27 +663,13 @@ class OpenAiResponsesTurnSession {
|
|
|
660
663
|
resetContinuation() {
|
|
661
664
|
clearResponsesContinuation(this.state);
|
|
662
665
|
}
|
|
663
|
-
continuationCheckpoint() {
|
|
664
|
-
if (!responsesPersistedContinuationEnabled(this.adapter.configuration))
|
|
665
|
-
return undefined;
|
|
666
|
-
const request = this.state.lastRequest;
|
|
667
|
-
const response = this.state.lastResponse;
|
|
668
|
-
if (request === undefined || response === undefined)
|
|
669
|
-
return this.state.restoredContinuation;
|
|
670
|
-
const baseline = [...request.input, ...response.outputItems];
|
|
671
|
-
return {
|
|
672
|
-
version: 2,
|
|
673
|
-
protocol: 'OPENAI_RESPONSES',
|
|
674
|
-
configurationHash: responsesContinuationIdentityHash(this.adapter.configuration),
|
|
675
|
-
responseId: response.responseId,
|
|
676
|
-
inputPrefixLength: baseline.length
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
666
|
prepareRequest(request) {
|
|
680
667
|
if (this.adapter.configuration.responsesTransport?.transport === 'WEBSOCKET' &&
|
|
681
668
|
this.state.socket !== undefined &&
|
|
682
|
-
this.state.socket.readyState !== WebSocket.OPEN)
|
|
669
|
+
this.state.socket.readyState !== WebSocket.OPEN) {
|
|
683
670
|
invalidateResponsesWebSocket(this.state, false);
|
|
671
|
+
clearResponsesContinuation(this.state);
|
|
672
|
+
}
|
|
684
673
|
const fullBody = responsesRequestBody(this.adapter.configuration, request, request.messages, this.adapter.prefixIdentity);
|
|
685
674
|
if (responsesContinuationEnabled(this.adapter.configuration)) {
|
|
686
675
|
const incremental = incrementalResponsesInput(this.state, fullBody);
|
|
@@ -743,8 +732,17 @@ class OpenAiResponsesTurnSession {
|
|
|
743
732
|
const endpoint = webSocketEndpoint(input.configuration.baseUrl);
|
|
744
733
|
const existingSocket = this.state.socket;
|
|
745
734
|
if (existingSocket !== undefined &&
|
|
746
|
-
(existingSocket.readyState !== WebSocket.OPEN || this.state.endpoint !== endpoint))
|
|
735
|
+
(existingSocket.readyState !== WebSocket.OPEN || this.state.endpoint !== endpoint)) {
|
|
747
736
|
invalidateResponsesWebSocket(this.state, false);
|
|
737
|
+
if (input.continuation) {
|
|
738
|
+
clearResponsesContinuation(this.state);
|
|
739
|
+
throw localContinuationReset();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (input.continuation && this.state.socket === undefined) {
|
|
743
|
+
clearResponsesContinuation(this.state);
|
|
744
|
+
throw localContinuationReset();
|
|
745
|
+
}
|
|
748
746
|
if (this.state.socket === undefined) {
|
|
749
747
|
const handshakeCredential = await acquireResponsesCredential(input.configuration, input.reason);
|
|
750
748
|
input.credentialAcquired(handshakeCredential.revision);
|
|
@@ -755,6 +753,10 @@ class OpenAiResponsesTurnSession {
|
|
|
755
753
|
if (requestCredential.revision !== this.state.credentialRevision ||
|
|
756
754
|
responsesCredentialFingerprint(requestCredential) !== this.state.credentialFingerprint) {
|
|
757
755
|
invalidateResponsesWebSocket(this.state, false);
|
|
756
|
+
if (input.continuation) {
|
|
757
|
+
clearResponsesContinuation(this.state);
|
|
758
|
+
throw localContinuationReset();
|
|
759
|
+
}
|
|
758
760
|
await this.connectWebSocket(endpoint, requestCredential, input.promptCacheKey, input.signal);
|
|
759
761
|
}
|
|
760
762
|
if (input.signal.aborted) {
|
|
@@ -814,6 +816,12 @@ class OpenAiResponsesTurnSession {
|
|
|
814
816
|
this.state.proxyType = proxyType;
|
|
815
817
|
}
|
|
816
818
|
}
|
|
819
|
+
function localContinuationReset() {
|
|
820
|
+
return new MarAgentError('MAR_AGENT_RESPONSES_CONTINUATION_RESET', 'The Responses connection changed before the incremental request was sent.');
|
|
821
|
+
}
|
|
822
|
+
function isLocalContinuationReset(error) {
|
|
823
|
+
return error instanceof MarAgentError && error.code === 'MAR_AGENT_RESPONSES_CONTINUATION_RESET';
|
|
824
|
+
}
|
|
817
825
|
function responsesRequestBody(configuration, request, messages, prefixIdentity = '') {
|
|
818
826
|
const tools = request.allowTools === false
|
|
819
827
|
? []
|
|
@@ -887,13 +895,8 @@ function uuidV5(namespace, value) {
|
|
|
887
895
|
function incrementalResponsesInput(state, current) {
|
|
888
896
|
const previous = state.lastRequest;
|
|
889
897
|
const completion = state.lastResponse;
|
|
890
|
-
if (previous === undefined || completion === undefined)
|
|
891
|
-
|
|
892
|
-
if (restored === undefined || current.input.length < restored.inputPrefixLength)
|
|
893
|
-
return undefined;
|
|
894
|
-
state.lastResponse = { responseId: restored.responseId, outputItems: [] };
|
|
895
|
-
return current.input.slice(restored.inputPrefixLength);
|
|
896
|
-
}
|
|
898
|
+
if (previous === undefined || completion === undefined)
|
|
899
|
+
return undefined;
|
|
897
900
|
const baseline = [...previous.input, ...completion.outputItems];
|
|
898
901
|
if (current.input.length < baseline.length)
|
|
899
902
|
return undefined;
|
|
@@ -905,25 +908,14 @@ function incrementalResponsesInput(state, current) {
|
|
|
905
908
|
function clearResponsesContinuation(state) {
|
|
906
909
|
delete state.lastRequest;
|
|
907
910
|
delete state.lastResponse;
|
|
908
|
-
delete state.restoredContinuation;
|
|
909
911
|
}
|
|
910
912
|
function hashResponsesValue(value) {
|
|
911
913
|
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
912
914
|
}
|
|
913
|
-
function responsesContinuationIdentityHash(configuration) {
|
|
914
|
-
return hashResponsesValue({
|
|
915
|
-
endpoint: new URL(responsesEndpoint(configuration.baseUrl)).toString(),
|
|
916
|
-
modelId: configuration.modelId,
|
|
917
|
-
responsesEncoding: configuration.responsesEncoding ?? 'STANDARD'
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
915
|
function responsesContinuationEnabled(configuration) {
|
|
921
916
|
return (configuration.responsesPreviousResponseId === true &&
|
|
922
917
|
configuration.responsesTransport?.transport === 'WEBSOCKET');
|
|
923
918
|
}
|
|
924
|
-
function responsesPersistedContinuationEnabled(configuration) {
|
|
925
|
-
return responsesContinuationEnabled(configuration) && configuration.responsesEncoding !== 'LITE';
|
|
926
|
-
}
|
|
927
919
|
function responsesCredentialFingerprint(credential) {
|
|
928
920
|
return hashResponsesValue({
|
|
929
921
|
bearer: credential.bearer.reveal(),
|
package/dist/sdk/agent.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { posix } from 'node:path';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
4
|
import { describeMarAgentError, MarAgentError } from '../error.js';
|
|
4
5
|
import { negotiateHost } from '../host/contracts.js';
|
|
5
6
|
import { AnthropicMessagesAdapter, isAnthropicRecoverableStreamError } from '../model/anthropic-messages.js';
|
|
@@ -101,7 +102,6 @@ export async function createMarAgent(options) {
|
|
|
101
102
|
sessionRuntime = {
|
|
102
103
|
sourceType: sessionSource.type,
|
|
103
104
|
adapterGeneration: modelGeneration,
|
|
104
|
-
responsesChain: undefined,
|
|
105
105
|
adapters: new Map()
|
|
106
106
|
};
|
|
107
107
|
sessionRuntimes.set(sessionId, sessionRuntime);
|
|
@@ -133,7 +133,6 @@ export async function createMarAgent(options) {
|
|
|
133
133
|
return;
|
|
134
134
|
const retainedByActiveSessions = activeAdaptersExcept(sessionId);
|
|
135
135
|
await closeModelAdapters([...sessionRuntime.adapters.values()].filter((adapter) => !retainedByActiveSessions.has(adapter)));
|
|
136
|
-
sessionRuntime.responsesChain = undefined;
|
|
137
136
|
sessionRuntime.adapters.clear();
|
|
138
137
|
sessionRuntime.adapterGeneration = generation;
|
|
139
138
|
};
|
|
@@ -253,9 +252,17 @@ export async function createMarAgent(options) {
|
|
|
253
252
|
const contextRecords = sessionState.contextRecords;
|
|
254
253
|
const contextualWorldStateGeneration = latestContextualWorldStateGeneration(contextRecords);
|
|
255
254
|
const restoredContextualWorldState = sessionRuntime.contextualWorldState;
|
|
255
|
+
const persistedContextualWorldState = parsePersistedContextualWorldState(await store.readContextualWorldState(sessionId));
|
|
256
256
|
let contextualWorldState;
|
|
257
|
-
if (restoredContextualWorldState
|
|
258
|
-
restoredContextualWorldState.generation
|
|
257
|
+
if (restoredContextualWorldState !== undefined &&
|
|
258
|
+
restoredContextualWorldState.generation === contextualWorldStateGeneration)
|
|
259
|
+
contextualWorldState = restoredContextualWorldState;
|
|
260
|
+
else if (persistedContextualWorldState !== undefined &&
|
|
261
|
+
persistedContextualWorldState.generation === contextualWorldStateGeneration) {
|
|
262
|
+
contextualWorldState = persistedContextualWorldState;
|
|
263
|
+
sessionRuntime.contextualWorldState = contextualWorldState;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
259
266
|
contextualWorldState = {
|
|
260
267
|
generation: contextualWorldStateGeneration,
|
|
261
268
|
initialized: false,
|
|
@@ -265,8 +272,6 @@ export async function createMarAgent(options) {
|
|
|
265
272
|
};
|
|
266
273
|
sessionRuntime.contextualWorldState = contextualWorldState;
|
|
267
274
|
}
|
|
268
|
-
else
|
|
269
|
-
contextualWorldState = restoredContextualWorldState;
|
|
270
275
|
const compactMayContainAgentInstructions = contextRecords.some((record) => compactSummaryFromCheckpoint(record) !== undefined);
|
|
271
276
|
const compactMayContainRetainedResources = compactHistoryMayContainRetainedResources(contextRecords);
|
|
272
277
|
const restored = await restoreMessages(store, sessionId, contextRecords, {
|
|
@@ -320,8 +325,6 @@ export async function createMarAgent(options) {
|
|
|
320
325
|
}));
|
|
321
326
|
const currentExecutionToolImages = [];
|
|
322
327
|
let currentExecutionUserIndex;
|
|
323
|
-
if (mode === 'compact')
|
|
324
|
-
sessionRuntime.responsesChain = undefined;
|
|
325
328
|
if (mode !== 'compact') {
|
|
326
329
|
await recordEnvironment();
|
|
327
330
|
const inputImageMetadata = input.images?.length
|
|
@@ -373,9 +376,6 @@ export async function createMarAgent(options) {
|
|
|
373
376
|
});
|
|
374
377
|
selectedAdapter = sessionAdapter(input.modelId ?? executionDefaultModelId, executionModels);
|
|
375
378
|
const selectedModel = executionModels.get(input.modelId ?? executionDefaultModelId);
|
|
376
|
-
const restoredContinuation = latestModelContinuationCheckpoint(contextRecords, selectedModel.id);
|
|
377
|
-
if (restoredContinuation !== undefined)
|
|
378
|
-
selectedAdapter.restoreContinuation?.(restoredContinuation);
|
|
379
379
|
if (options.adapterFactory !== undefined &&
|
|
380
380
|
selectedAdapter.createTurnSession === undefined) {
|
|
381
381
|
executionScopedAdapters.add(selectedAdapter);
|
|
@@ -487,6 +487,7 @@ export async function createMarAgent(options) {
|
|
|
487
487
|
};
|
|
488
488
|
};
|
|
489
489
|
let currentResourceSnapshot = retainedSessionResourcesSnapshot(await loadRetainedSessionResources());
|
|
490
|
+
const previousContextualWorldState = contextualWorldState;
|
|
490
491
|
contextualWorldState = updateContextualWorldState(contextualWorldState, agentInstructionInsertIndex, {
|
|
491
492
|
instructionSnapshot: currentInstructionSnapshot,
|
|
492
493
|
instructionContent: renderAgentInstructions(instructionEntries, compactMayContainAgentInstructions ||
|
|
@@ -495,6 +496,8 @@ export async function createMarAgent(options) {
|
|
|
495
496
|
previousMayContainResources: compactMayContainRetainedResources
|
|
496
497
|
});
|
|
497
498
|
sessionRuntime.contextualWorldState = contextualWorldState;
|
|
499
|
+
if (!isDeepStrictEqual(previousContextualWorldState, contextualWorldState))
|
|
500
|
+
await persistContextualWorldState(store, sessionId, contextualWorldState);
|
|
498
501
|
const buildExecutionSystemPrompt = (availableTools = executionTools, promptMode = mode) => buildSystemPrompt({
|
|
499
502
|
mode: promptMode,
|
|
500
503
|
platform: description.platform,
|
|
@@ -659,14 +662,12 @@ export async function createMarAgent(options) {
|
|
|
659
662
|
}
|
|
660
663
|
catch (error) {
|
|
661
664
|
if (await recoverModelStream(error, compactionRetryState, operation.signal)) {
|
|
662
|
-
sessionRuntime.responsesChain = undefined;
|
|
663
665
|
continue;
|
|
664
666
|
}
|
|
665
667
|
if (isProviderContextOverflow(error) && usingNativeMessages) {
|
|
666
668
|
usingNativeMessages = false;
|
|
667
669
|
compactMessages = fallbackMessages();
|
|
668
670
|
compactionRetryState.attempts = 0;
|
|
669
|
-
sessionRuntime.responsesChain = undefined;
|
|
670
671
|
continue;
|
|
671
672
|
}
|
|
672
673
|
if (isProviderContextOverflow(error))
|
|
@@ -707,7 +708,7 @@ export async function createMarAgent(options) {
|
|
|
707
708
|
previousMayContainResources: compactSummary.includes('# Retained Session resources')
|
|
708
709
|
});
|
|
709
710
|
sessionRuntime.contextualWorldState = contextualWorldState;
|
|
710
|
-
|
|
711
|
+
await persistContextualWorldState(store, sessionId, contextualWorldState);
|
|
711
712
|
lastServerContextTokens = undefined;
|
|
712
713
|
lastServerUsage = undefined;
|
|
713
714
|
await emit({ type: 'context.compacted', compactionId });
|
|
@@ -731,7 +732,6 @@ export async function createMarAgent(options) {
|
|
|
731
732
|
await appendMailboxMessages();
|
|
732
733
|
let stop;
|
|
733
734
|
let sawTool = false;
|
|
734
|
-
let responseId;
|
|
735
735
|
let latestUsage;
|
|
736
736
|
const pendingTools = [];
|
|
737
737
|
let incompleteToolCall = false;
|
|
@@ -741,27 +741,9 @@ export async function createMarAgent(options) {
|
|
|
741
741
|
const systemPrompt = mode === 'compact'
|
|
742
742
|
? buildExecutionSystemPrompt(roundTools, 'run')
|
|
743
743
|
: buildExecutionSystemPrompt(roundTools);
|
|
744
|
-
const requestSignature = createHash('sha256')
|
|
745
|
-
.update(stableJson({
|
|
746
|
-
modelId: selectedModel.id,
|
|
747
|
-
systemPrompt,
|
|
748
|
-
contextualWorldState: contextualWorldState.entries,
|
|
749
|
-
tools: roundTools,
|
|
750
|
-
reasoningEffort
|
|
751
|
-
}))
|
|
752
|
-
.digest('hex');
|
|
753
744
|
const requestContextMessages = pendingOutputContinuation === undefined
|
|
754
745
|
? messages
|
|
755
746
|
: [...messages, pendingOutputContinuation];
|
|
756
|
-
const continuation = selectedModel.responsesPreviousResponseId &&
|
|
757
|
-
sessionRuntime.responsesChain?.modelId === selectedModel.id &&
|
|
758
|
-
sessionRuntime.responsesChain.requestSignature === requestSignature &&
|
|
759
|
-
sessionRuntime.responsesChain.messageCount <= requestContextMessages.length
|
|
760
|
-
? {
|
|
761
|
-
previousResponseId: sessionRuntime.responsesChain.responseId,
|
|
762
|
-
deltaMessages: requestContextMessages.slice(sessionRuntime.responsesChain.messageCount)
|
|
763
|
-
}
|
|
764
|
-
: undefined;
|
|
765
747
|
const modelMessages = mode === 'compact'
|
|
766
748
|
? normalizeMessagesForModel(requestContextMessages, { supportsImages: false })
|
|
767
749
|
: normalizeMessagesForModel(insertContextualWorldState(requestContextMessages, contextualWorldState.entries), {
|
|
@@ -808,16 +790,13 @@ export async function createMarAgent(options) {
|
|
|
808
790
|
? {}
|
|
809
791
|
: { allowTools: false }),
|
|
810
792
|
promptCacheKey: sessionId,
|
|
811
|
-
...(continuation ? { continuation } : {}),
|
|
812
793
|
reasoningEffort,
|
|
813
794
|
...(modelInputImages?.length ? { images: modelInputImages } : {}),
|
|
814
795
|
onAttemptDiagnostic: recordModelAttempt(mode === 'compact' ? 'COMPACTION' : 'EXECUTION')
|
|
815
796
|
})) {
|
|
816
797
|
if (event.type !== 'response.started' && event.type !== 'usage')
|
|
817
798
|
sawModelSemanticEvent = true;
|
|
818
|
-
if (event.type === 'response.
|
|
819
|
-
responseId = event.responseId;
|
|
820
|
-
else if (event.type === 'response.item') {
|
|
799
|
+
if (event.type === 'response.item') {
|
|
821
800
|
// Compaction keeps only its successful checkpoint, never intermediate native output.
|
|
822
801
|
if (mode === 'compact')
|
|
823
802
|
continue;
|
|
@@ -955,8 +934,6 @@ export async function createMarAgent(options) {
|
|
|
955
934
|
catch (error) {
|
|
956
935
|
planOutputPrefixes.clear();
|
|
957
936
|
if (await recoverModelStream(error, modelRetryState, manualCompaction?.signal ?? controller.signal)) {
|
|
958
|
-
sessionRuntime.responsesChain = undefined;
|
|
959
|
-
responseId = undefined;
|
|
960
937
|
finalAnswer = '';
|
|
961
938
|
if (mode === 'compact')
|
|
962
939
|
messages.splice(roundMessageCount);
|
|
@@ -971,16 +948,13 @@ export async function createMarAgent(options) {
|
|
|
971
948
|
!sawModelSemanticEvent &&
|
|
972
949
|
isProviderContextOverflow(error) &&
|
|
973
950
|
useManualCompactionFallback?.()) {
|
|
974
|
-
sessionRuntime.responsesChain = undefined;
|
|
975
951
|
modelRetryState.attempts = 0;
|
|
976
|
-
responseId = undefined;
|
|
977
952
|
finalAnswer = '';
|
|
978
953
|
continue;
|
|
979
954
|
}
|
|
980
955
|
else if (mode !== 'compact' &&
|
|
981
956
|
!sawModelSemanticEvent &&
|
|
982
957
|
isProviderContextOverflow(error)) {
|
|
983
|
-
sessionRuntime.responsesChain = undefined;
|
|
984
958
|
if (!recoveringFromContextOverflow && (await compactContext())) {
|
|
985
959
|
modelRetryState.attempts = 0;
|
|
986
960
|
recoveringFromContextOverflow = true;
|
|
@@ -992,26 +966,6 @@ export async function createMarAgent(options) {
|
|
|
992
966
|
throw error;
|
|
993
967
|
}
|
|
994
968
|
recoveringFromContextOverflow = false;
|
|
995
|
-
if (selectedModel.responsesPreviousResponseId && responseId)
|
|
996
|
-
sessionRuntime.responsesChain = {
|
|
997
|
-
modelId: selectedModel.id,
|
|
998
|
-
responseId,
|
|
999
|
-
requestSignature,
|
|
1000
|
-
messageCount: messages.length
|
|
1001
|
-
};
|
|
1002
|
-
else if (selectedModel.responsesPreviousResponseId)
|
|
1003
|
-
sessionRuntime.responsesChain = undefined;
|
|
1004
|
-
const continuationCheckpoint = selected.continuationCheckpoint?.();
|
|
1005
|
-
if (mode !== 'compact' && continuationCheckpoint !== undefined)
|
|
1006
|
-
await store.append(sessionId, {
|
|
1007
|
-
type: 'responses.continuation',
|
|
1008
|
-
payload: {
|
|
1009
|
-
modelId: selectedModel.id,
|
|
1010
|
-
checkpoint: continuationCheckpoint
|
|
1011
|
-
},
|
|
1012
|
-
executionId,
|
|
1013
|
-
turnId
|
|
1014
|
-
});
|
|
1015
969
|
if (pendingTools.length === 0 && mailbox.length > 0) {
|
|
1016
970
|
await appendMailboxMessages();
|
|
1017
971
|
finalAnswer = '';
|
|
@@ -1186,7 +1140,6 @@ export async function createMarAgent(options) {
|
|
|
1186
1140
|
}
|
|
1187
1141
|
});
|
|
1188
1142
|
if (committed) {
|
|
1189
|
-
sessionRuntime.responsesChain = undefined;
|
|
1190
1143
|
selected.resetContinuation?.();
|
|
1191
1144
|
const refreshed = await restoreMessages(store, sessionId, committed.records, {
|
|
1192
1145
|
includeImages: true,
|
|
@@ -1208,6 +1161,7 @@ export async function createMarAgent(options) {
|
|
|
1208
1161
|
previousMayContainResources: compactHistoryMayContainRetainedResources(committed.records)
|
|
1209
1162
|
});
|
|
1210
1163
|
sessionRuntime.contextualWorldState = contextualWorldState;
|
|
1164
|
+
await persistContextualWorldState(store, sessionId, contextualWorldState);
|
|
1211
1165
|
}
|
|
1212
1166
|
}
|
|
1213
1167
|
if (controller.signal.aborted) {
|
|
@@ -1402,9 +1356,6 @@ export async function createMarAgent(options) {
|
|
|
1402
1356
|
for (const [modelId, adapter] of runtime.adapters)
|
|
1403
1357
|
if (!retainedAdapters.has(adapter))
|
|
1404
1358
|
runtime.adapters.delete(modelId);
|
|
1405
|
-
if (runtime.responsesChain !== undefined &&
|
|
1406
|
-
!retainedAdapters.has(runtime.adapters.get(runtime.responsesChain.modelId)))
|
|
1407
|
-
runtime.responsesChain = undefined;
|
|
1408
1359
|
}
|
|
1409
1360
|
await closeModelAdapters(adapters);
|
|
1410
1361
|
if (disposed)
|
|
@@ -1572,44 +1523,9 @@ function createModelTurnSession(adapter) {
|
|
|
1572
1523
|
}
|
|
1573
1524
|
},
|
|
1574
1525
|
resetContinuation: () => turn.resetContinuation?.(),
|
|
1575
|
-
continuationCheckpoint: () => turn.continuationCheckpoint?.(),
|
|
1576
1526
|
close: () => turn.close()
|
|
1577
1527
|
};
|
|
1578
1528
|
}
|
|
1579
|
-
function latestModelContinuationCheckpoint(records, modelId) {
|
|
1580
|
-
let checkpoint;
|
|
1581
|
-
for (const record of records) {
|
|
1582
|
-
if (record.type === 'compact.completed' || record.type === 'context.gc.completed') {
|
|
1583
|
-
checkpoint = undefined;
|
|
1584
|
-
continue;
|
|
1585
|
-
}
|
|
1586
|
-
if (record.type !== 'responses.continuation')
|
|
1587
|
-
continue;
|
|
1588
|
-
const payload = record.payload;
|
|
1589
|
-
if (payload === null || typeof payload !== 'object' || Array.isArray(payload))
|
|
1590
|
-
continue;
|
|
1591
|
-
const candidateModelId = payload.modelId;
|
|
1592
|
-
const candidate = payload.checkpoint;
|
|
1593
|
-
if (candidateModelId !== modelId || !isModelContinuationCheckpoint(candidate))
|
|
1594
|
-
continue;
|
|
1595
|
-
checkpoint = candidate;
|
|
1596
|
-
}
|
|
1597
|
-
return checkpoint;
|
|
1598
|
-
}
|
|
1599
|
-
function isModelContinuationCheckpoint(value) {
|
|
1600
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
1601
|
-
return false;
|
|
1602
|
-
const checkpoint = value;
|
|
1603
|
-
return (checkpoint.version === 2 &&
|
|
1604
|
-
checkpoint.protocol === 'OPENAI_RESPONSES' &&
|
|
1605
|
-
isBoundedString(checkpoint.configurationHash, 64) &&
|
|
1606
|
-
isBoundedString(checkpoint.responseId, 1_024) &&
|
|
1607
|
-
Number.isSafeInteger(checkpoint.inputPrefixLength) &&
|
|
1608
|
-
checkpoint.inputPrefixLength >= 0);
|
|
1609
|
-
}
|
|
1610
|
-
function isBoundedString(value, maximumLength) {
|
|
1611
|
-
return typeof value === 'string' && value.length > 0 && value.length <= maximumLength;
|
|
1612
|
-
}
|
|
1613
1529
|
function modelAttemptFailureDiagnostic(diagnostic, summary) {
|
|
1614
1530
|
if (summary === undefined)
|
|
1615
1531
|
return diagnostic;
|
|
@@ -1906,6 +1822,73 @@ function latestContextualWorldStateGeneration(records) {
|
|
|
1906
1822
|
generation = record.recordId;
|
|
1907
1823
|
return generation;
|
|
1908
1824
|
}
|
|
1825
|
+
const MAX_CONTEXTUAL_WORLD_STATE_ENTRIES = 1_024;
|
|
1826
|
+
const MAX_CONTEXTUAL_WORLD_STATE_CONTENT_BYTES = 8 * 1024 * 1024;
|
|
1827
|
+
function parsePersistedContextualWorldState(payload) {
|
|
1828
|
+
if (payload === null || typeof payload !== 'object' || Array.isArray(payload))
|
|
1829
|
+
return undefined;
|
|
1830
|
+
const record = payload;
|
|
1831
|
+
if (record.version !== 1)
|
|
1832
|
+
return undefined;
|
|
1833
|
+
const generation = record.generation === null
|
|
1834
|
+
? undefined
|
|
1835
|
+
: typeof record.generation === 'string' && record.generation.length <= 128
|
|
1836
|
+
? record.generation
|
|
1837
|
+
: null;
|
|
1838
|
+
if (generation === null)
|
|
1839
|
+
return undefined;
|
|
1840
|
+
const instructionSnapshot = nullableWorldStateString(record.instructionSnapshot);
|
|
1841
|
+
const resourceSnapshot = nullableWorldStateString(record.resourceSnapshot);
|
|
1842
|
+
if (instructionSnapshot === null || resourceSnapshot === null || !Array.isArray(record.entries))
|
|
1843
|
+
return undefined;
|
|
1844
|
+
if (record.entries.length > MAX_CONTEXTUAL_WORLD_STATE_ENTRIES)
|
|
1845
|
+
return undefined;
|
|
1846
|
+
const entries = [];
|
|
1847
|
+
let contentBytes = 0;
|
|
1848
|
+
for (const value of record.entries) {
|
|
1849
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
1850
|
+
return undefined;
|
|
1851
|
+
const entry = value;
|
|
1852
|
+
if (!Number.isSafeInteger(entry.index) ||
|
|
1853
|
+
entry.index < 0 ||
|
|
1854
|
+
(entry.kind !== 'instructions' && entry.kind !== 'resources') ||
|
|
1855
|
+
typeof entry.content !== 'string' ||
|
|
1856
|
+
entry.content.length === 0)
|
|
1857
|
+
return undefined;
|
|
1858
|
+
contentBytes += Buffer.byteLength(entry.content);
|
|
1859
|
+
if (contentBytes > MAX_CONTEXTUAL_WORLD_STATE_CONTENT_BYTES)
|
|
1860
|
+
return undefined;
|
|
1861
|
+
entries.push({
|
|
1862
|
+
index: entry.index,
|
|
1863
|
+
kind: entry.kind,
|
|
1864
|
+
content: entry.content
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
return {
|
|
1868
|
+
generation,
|
|
1869
|
+
initialized: true,
|
|
1870
|
+
instructionSnapshot,
|
|
1871
|
+
resourceSnapshot,
|
|
1872
|
+
entries
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
function nullableWorldStateString(value) {
|
|
1876
|
+
if (value === null)
|
|
1877
|
+
return undefined;
|
|
1878
|
+
return typeof value === 'string' &&
|
|
1879
|
+
Buffer.byteLength(value) <= MAX_CONTEXTUAL_WORLD_STATE_CONTENT_BYTES
|
|
1880
|
+
? value
|
|
1881
|
+
: null;
|
|
1882
|
+
}
|
|
1883
|
+
async function persistContextualWorldState(store, sessionId, state) {
|
|
1884
|
+
await store.writeContextualWorldState(sessionId, {
|
|
1885
|
+
version: 1,
|
|
1886
|
+
generation: state.generation ?? null,
|
|
1887
|
+
instructionSnapshot: state.instructionSnapshot ?? null,
|
|
1888
|
+
resourceSnapshot: state.resourceSnapshot ?? null,
|
|
1889
|
+
entries: state.entries
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1909
1892
|
function compactHistoryMayContainRetainedResources(records) {
|
|
1910
1893
|
return records.some((record) => compactSummaryFromCheckpoint(record)?.includes('# Retained Session resources'));
|
|
1911
1894
|
}
|
|
@@ -94,6 +94,8 @@ export declare class JsonlSessionStore {
|
|
|
94
94
|
source?: SessionSource;
|
|
95
95
|
}): Promise<SessionMeta>;
|
|
96
96
|
sessionDirectory(sessionId: string): string;
|
|
97
|
+
readContextualWorldState(sessionId: string): Promise<unknown | undefined>;
|
|
98
|
+
writeContextualWorldState(sessionId: string, state: unknown): Promise<void>;
|
|
97
99
|
persistInputImages(sessionId: string, images: readonly SessionInputImage[]): Promise<SessionInputImageMetadata[]>;
|
|
98
100
|
readInputImages(sessionId: string, value: unknown): Promise<RestoredSessionInputImage[]>;
|
|
99
101
|
readInputImage(sessionId: string, input: {
|
|
@@ -12,6 +12,8 @@ const REVERSE_READ_CHUNK_BYTES = 64 * 1024;
|
|
|
12
12
|
const MAX_INPUT_IMAGES = 5;
|
|
13
13
|
const MAX_INPUT_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
14
14
|
const MAX_INPUT_IMAGES_BYTES = 10 * 1024 * 1024;
|
|
15
|
+
const MAX_CONTEXTUAL_WORLD_STATE_BYTES = 8 * 1024 * 1024;
|
|
16
|
+
const CONTEXTUAL_WORLD_STATE_FILE = 'context-world-state.json';
|
|
15
17
|
const INPUT_IMAGE_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
|
|
16
18
|
export class JsonlSessionStore {
|
|
17
19
|
static #instances = new Map();
|
|
@@ -54,6 +56,8 @@ export class JsonlSessionStore {
|
|
|
54
56
|
catch {
|
|
55
57
|
// A missing index starts empty; JSONL remains authoritative for explicit session reads.
|
|
56
58
|
}
|
|
59
|
+
for (const session of index.sessions)
|
|
60
|
+
await recoverContextualWorldStateTransactions(root, session);
|
|
57
61
|
return {
|
|
58
62
|
root,
|
|
59
63
|
index,
|
|
@@ -157,6 +161,57 @@ export class JsonlSessionStore {
|
|
|
157
161
|
throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
|
|
158
162
|
return join(this.#shared.root, 'sessions', sessionId);
|
|
159
163
|
}
|
|
164
|
+
async readContextualWorldState(sessionId) {
|
|
165
|
+
const path = join(this.sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
|
|
166
|
+
try {
|
|
167
|
+
const pathInfo = await lstat(path);
|
|
168
|
+
if (!pathInfo.isFile())
|
|
169
|
+
return undefined;
|
|
170
|
+
const file = await open(path, 'r');
|
|
171
|
+
try {
|
|
172
|
+
const info = await file.stat();
|
|
173
|
+
if (!info.isFile() || info.size > MAX_CONTEXTUAL_WORLD_STATE_BYTES)
|
|
174
|
+
return undefined;
|
|
175
|
+
const envelope = JSON.parse(await file.readFile('utf8'));
|
|
176
|
+
const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
|
|
177
|
+
if (envelope.version !== 1 ||
|
|
178
|
+
!Number.isSafeInteger(envelope.historySequence) ||
|
|
179
|
+
envelope.historySequence < 1 ||
|
|
180
|
+
meta === undefined ||
|
|
181
|
+
envelope.historySequence >= meta.nextSequence)
|
|
182
|
+
return undefined;
|
|
183
|
+
return envelope.state;
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await file.close();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async writeContextualWorldState(sessionId, state) {
|
|
194
|
+
const directory = this.sessionDirectory(sessionId);
|
|
195
|
+
const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
|
|
196
|
+
if (!meta)
|
|
197
|
+
throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
|
|
198
|
+
const path = join(directory, CONTEXTUAL_WORLD_STATE_FILE);
|
|
199
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
200
|
+
const serialized = JSON.stringify({
|
|
201
|
+
version: 1,
|
|
202
|
+
historySequence: meta.nextSequence - 1,
|
|
203
|
+
state
|
|
204
|
+
});
|
|
205
|
+
if (Buffer.byteLength(serialized) > MAX_CONTEXTUAL_WORLD_STATE_BYTES)
|
|
206
|
+
throw new MarAgentError('MAR_AGENT_HOST_LIMIT_EXCEEDED', 'Contextual world state exceeds the storage limit.');
|
|
207
|
+
try {
|
|
208
|
+
await writeFile(temporary, serialized, { mode: 0o600, flag: 'wx' });
|
|
209
|
+
await rename(temporary, path);
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
await rm(temporary, { force: true });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
160
215
|
async persistInputImages(sessionId, images) {
|
|
161
216
|
this.#assertOpen();
|
|
162
217
|
if (images.length === 0)
|
|
@@ -373,6 +428,10 @@ export class JsonlSessionStore {
|
|
|
373
428
|
.filter((record) => record.type !== 'responses.continuation');
|
|
374
429
|
const staged = await stageRollbackSegments(this.sessionDirectory(sessionId), retained);
|
|
375
430
|
const transactionId = randomUUID();
|
|
431
|
+
const targetNextSequence = (retained.at(-1)?.sequence ?? 0) + 1;
|
|
432
|
+
const contextualWorldStatePath = join(this.sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
|
|
433
|
+
const contextualWorldStateBackup = `${contextualWorldStatePath}.${targetNextSequence}.${transactionId}.rollback-backup`;
|
|
434
|
+
let contextualWorldStateStaged = false;
|
|
376
435
|
let generatedTrash = [];
|
|
377
436
|
let inputTrash = [];
|
|
378
437
|
const originalMeta = structuredClone(meta);
|
|
@@ -386,6 +445,7 @@ export class JsonlSessionStore {
|
|
|
386
445
|
const installedPaths = [];
|
|
387
446
|
let committed = false;
|
|
388
447
|
try {
|
|
448
|
+
contextualWorldStateStaged = await renameIfExists(contextualWorldStatePath, contextualWorldStateBackup);
|
|
389
449
|
generatedTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'generated_images', new Set(generatedImageFileNames(retained)), transactionId);
|
|
390
450
|
inputTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'input_images', new Set(inputImageReferencesFromRecords(retained).map((image) => image.fileName)), transactionId);
|
|
391
451
|
for (const item of backups)
|
|
@@ -398,7 +458,7 @@ export class JsonlSessionStore {
|
|
|
398
458
|
const completedAt = new Date().toISOString();
|
|
399
459
|
meta.title = retainedSessionTitle(retained, meta.title);
|
|
400
460
|
meta.lastActivityAt = completedAt;
|
|
401
|
-
meta.nextSequence =
|
|
461
|
+
meta.nextSequence = targetNextSequence;
|
|
402
462
|
meta.automaticTitlePending = state.automaticTitlePending;
|
|
403
463
|
meta.resume = state.resume;
|
|
404
464
|
meta.segments = staged.segments;
|
|
@@ -407,6 +467,8 @@ export class JsonlSessionStore {
|
|
|
407
467
|
this.#shared.indexDirty = true;
|
|
408
468
|
await this.#saveIndex();
|
|
409
469
|
committed = true;
|
|
470
|
+
if (contextualWorldStateStaged)
|
|
471
|
+
await rm(contextualWorldStateBackup, { force: true });
|
|
410
472
|
await Promise.all([
|
|
411
473
|
...backups.map((item) => rm(item.backup, { force: true }).catch(() => undefined)),
|
|
412
474
|
...generatedTrash.map((item) => rm(item.trash, { force: true }).catch(() => undefined)),
|
|
@@ -425,6 +487,10 @@ export class JsonlSessionStore {
|
|
|
425
487
|
if (!committed) {
|
|
426
488
|
await Promise.all(installedPaths.map((path) => rm(path, { force: true }))).catch(() => undefined);
|
|
427
489
|
await restoreRenamedFiles(backups.map((item) => ({ from: item.backup, to: item.path })));
|
|
490
|
+
if (contextualWorldStateStaged)
|
|
491
|
+
await restoreRenamedFiles([
|
|
492
|
+
{ from: contextualWorldStateBackup, to: contextualWorldStatePath }
|
|
493
|
+
]);
|
|
428
494
|
await restoreRenamedFiles([...generatedTrash, ...inputTrash]);
|
|
429
495
|
restoreSessionMeta(meta, originalMeta);
|
|
430
496
|
this.#shared.index.revision = originalIndexRevision;
|
|
@@ -489,6 +555,9 @@ export class JsonlSessionStore {
|
|
|
489
555
|
];
|
|
490
556
|
const staged = await stageRollbackSegments(this.sessionDirectory(sessionId), retained);
|
|
491
557
|
const transactionId = randomUUID();
|
|
558
|
+
const contextualWorldStatePath = join(this.sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
|
|
559
|
+
const contextualWorldStateBackup = `${contextualWorldStatePath}.2.${transactionId}.clear-backup`;
|
|
560
|
+
let contextualWorldStateStaged = false;
|
|
492
561
|
let generatedTrash = [];
|
|
493
562
|
let inputTrash = [];
|
|
494
563
|
const originalMeta = structuredClone(meta);
|
|
@@ -502,6 +571,7 @@ export class JsonlSessionStore {
|
|
|
502
571
|
const installedPaths = [];
|
|
503
572
|
let committed = false;
|
|
504
573
|
try {
|
|
574
|
+
contextualWorldStateStaged = await renameIfExists(contextualWorldStatePath, contextualWorldStateBackup);
|
|
505
575
|
generatedTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'generated_images', new Set(), transactionId);
|
|
506
576
|
inputTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'input_images', new Set(), transactionId);
|
|
507
577
|
for (const item of backups)
|
|
@@ -519,6 +589,8 @@ export class JsonlSessionStore {
|
|
|
519
589
|
this.#shared.indexDirty = true;
|
|
520
590
|
await this.#saveIndex();
|
|
521
591
|
committed = true;
|
|
592
|
+
if (contextualWorldStateStaged)
|
|
593
|
+
await rm(contextualWorldStateBackup, { force: true });
|
|
522
594
|
await Promise.all([
|
|
523
595
|
...backups.map((item) => rm(item.backup, { force: true }).catch(() => undefined)),
|
|
524
596
|
...generatedTrash.map((item) => rm(item.trash, { force: true }).catch(() => undefined)),
|
|
@@ -537,6 +609,10 @@ export class JsonlSessionStore {
|
|
|
537
609
|
if (!committed) {
|
|
538
610
|
await Promise.all(installedPaths.map((path) => rm(path, { force: true }))).catch(() => undefined);
|
|
539
611
|
await restoreRenamedFiles(backups.map((item) => ({ from: item.backup, to: item.path })));
|
|
612
|
+
if (contextualWorldStateStaged)
|
|
613
|
+
await restoreRenamedFiles([
|
|
614
|
+
{ from: contextualWorldStateBackup, to: contextualWorldStatePath }
|
|
615
|
+
]);
|
|
540
616
|
await restoreRenamedFiles([...generatedTrash, ...inputTrash]);
|
|
541
617
|
restoreSessionMeta(meta, originalMeta);
|
|
542
618
|
this.#shared.index.revision = originalIndexRevision;
|
|
@@ -1072,6 +1148,49 @@ async function sessionDirectoryExists(path) {
|
|
|
1072
1148
|
throw error;
|
|
1073
1149
|
}
|
|
1074
1150
|
}
|
|
1151
|
+
async function renameIfExists(from, to) {
|
|
1152
|
+
try {
|
|
1153
|
+
await rename(from, to);
|
|
1154
|
+
return true;
|
|
1155
|
+
}
|
|
1156
|
+
catch (error) {
|
|
1157
|
+
if (error.code === 'ENOENT')
|
|
1158
|
+
return false;
|
|
1159
|
+
throw error;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
async function recoverContextualWorldStateTransactions(root, meta) {
|
|
1163
|
+
const directory = join(root, 'sessions', meta.id);
|
|
1164
|
+
let names;
|
|
1165
|
+
try {
|
|
1166
|
+
names = await readdir(directory);
|
|
1167
|
+
}
|
|
1168
|
+
catch (error) {
|
|
1169
|
+
if (error.code === 'ENOENT')
|
|
1170
|
+
return;
|
|
1171
|
+
throw error;
|
|
1172
|
+
}
|
|
1173
|
+
const pattern = /^context-world-state\.json\.(\d+)\.[0-9a-f-]+\.(?:rollback|clear)-backup$/u;
|
|
1174
|
+
const backups = names.filter((name) => pattern.test(name));
|
|
1175
|
+
if (backups.length === 0)
|
|
1176
|
+
return;
|
|
1177
|
+
const eventNames = names.filter((name) => /^events-\d{6}\.jsonl$/u.test(name)).sort();
|
|
1178
|
+
const latestEventName = eventNames.at(-1);
|
|
1179
|
+
if (latestEventName === undefined)
|
|
1180
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.');
|
|
1181
|
+
const latestRecords = await readRecords(join(directory, latestEventName));
|
|
1182
|
+
const actualNextSequence = (latestRecords.at(-1)?.sequence ?? 0) + 1;
|
|
1183
|
+
const canonical = join(directory, CONTEXTUAL_WORLD_STATE_FILE);
|
|
1184
|
+
for (const name of backups) {
|
|
1185
|
+
const match = pattern.exec(name);
|
|
1186
|
+
const backup = join(directory, name);
|
|
1187
|
+
const targetNextSequence = Number(match[1]);
|
|
1188
|
+
if (actualNextSequence === targetNextSequence || (await sessionDirectoryExists(canonical)))
|
|
1189
|
+
await rm(backup, { force: true });
|
|
1190
|
+
else
|
|
1191
|
+
await rename(backup, canonical);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1075
1194
|
function generatedImageFileNames(records) {
|
|
1076
1195
|
const names = new Set();
|
|
1077
1196
|
for (const record of records) {
|