@myagentroam/agent 0.9.157 → 0.9.159
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/agent-core/model/contracts.d.ts +9 -0
- package/dist/agent-core/model/openai-responses.d.ts +3 -1
- package/dist/agent-core/model/openai-responses.js +175 -85
- package/dist/agent-core/session/catalog.d.ts +3 -0
- package/dist/agent-core/session/catalog.js +1 -0
- package/dist/agent-core/session/jsonl-store.d.ts +6 -2
- package/dist/agent-core/session/jsonl-store.js +180 -8
- package/dist/agent-core/tools/code-mode/quickjs-engine.js +12 -0
- package/dist/agent-core/tools/local/view-image.js +108 -2
- package/package.json +1 -1
|
@@ -36,6 +36,15 @@ export interface ModelAttemptDiagnostic {
|
|
|
36
36
|
connectionId?: string;
|
|
37
37
|
connectionReused?: boolean;
|
|
38
38
|
proxyType?: 'DIRECT' | 'SOCKS5';
|
|
39
|
+
/** Actual serialized WebSocket request size, including inline image Base64. */
|
|
40
|
+
requestBytes?: number;
|
|
41
|
+
imageCount?: number;
|
|
42
|
+
/** Decoded byte count of inline input images; external URLs do not add bytes. */
|
|
43
|
+
imageBytes?: number;
|
|
44
|
+
requestFrameCount?: number;
|
|
45
|
+
sentFrameCount?: number;
|
|
46
|
+
webSocketPhase?: 'CONNECT' | 'SEND' | 'RECEIVE';
|
|
47
|
+
webSocketCloseCode?: number;
|
|
39
48
|
durationMs?: number;
|
|
40
49
|
errorCode?: string;
|
|
41
50
|
errorMessage?: string;
|
|
@@ -25,6 +25,7 @@ interface PreparedResponsesRequest {
|
|
|
25
25
|
readonly body: ResponsesRequestBody;
|
|
26
26
|
readonly continuation: boolean;
|
|
27
27
|
}
|
|
28
|
+
type ResponsesWebSocketDiagnostic = Pick<ModelAttemptDiagnostic, 'requestBytes' | 'imageCount' | 'imageBytes' | 'requestFrameCount' | 'sentFrameCount' | 'webSocketPhase' | 'webSocketCloseCode'>;
|
|
28
29
|
export declare class OpenAiResponsesAdapter implements ModelAdapter {
|
|
29
30
|
#private;
|
|
30
31
|
constructor(configuration: MarAgentModelConfiguration);
|
|
@@ -51,7 +52,8 @@ declare class OpenAiResponsesTurnSession implements ModelTurnSession {
|
|
|
51
52
|
fullRequestBody(): ResponsesRequestBody;
|
|
52
53
|
currentTurnState(): string | undefined;
|
|
53
54
|
beginAttempt(): void;
|
|
54
|
-
|
|
55
|
+
captureWebSocketDiagnostic(diagnostic: ResponsesWebSocketDiagnostic): void;
|
|
56
|
+
connectionDiagnostic(): Pick<ModelAttemptDiagnostic, 'connectionId' | 'connectionReused' | 'proxyType'> & ResponsesWebSocketDiagnostic;
|
|
55
57
|
captureTurnState(value: unknown): void;
|
|
56
58
|
withTurnStateMetadata(body: ResponsesRequestBody): ResponsesRequestBody;
|
|
57
59
|
invalidateWebSocket(): void;
|
|
@@ -8,6 +8,7 @@ import { isRetryableUpstreamOverloadCode, number, parseSse, record, text, observ
|
|
|
8
8
|
import { connectSocks5Target, fetchModelResponse, isRetryableModelHttpStatus, isRetryableModelTransportError, modelRetryMaxAttempts, isRetryableModelRequestError, modelRetryDelay, waitForModelRetry } from './http.js';
|
|
9
9
|
import { acquireModelCredential, appendModelCredentialHeaders } from './runtime-credential.js';
|
|
10
10
|
const WEBSOCKET_IDLE_TIMEOUT_MS = 300_000;
|
|
11
|
+
const WEBSOCKET_SEND_FRAME_BYTES = 1024 * 1024;
|
|
11
12
|
export const OPENAI_RESPONSES_EARLY_EOF_REASON = 'OPENAI_RESPONSES_EARLY_EOF';
|
|
12
13
|
export const OPENAI_RESPONSES_WEBSOCKET_CONNECTION_LIMIT_REASON = 'OPENAI_RESPONSES_WEBSOCKET_CONNECTION_LIMIT';
|
|
13
14
|
const X_CODEX_TURN_STATE = 'x-codex-turn-state';
|
|
@@ -624,6 +625,7 @@ class OpenAiResponsesTurnSession {
|
|
|
624
625
|
#fullBody;
|
|
625
626
|
#completedResponseId;
|
|
626
627
|
#connectionDiagnostic;
|
|
628
|
+
#webSocketDiagnostic;
|
|
627
629
|
constructor(adapter, state, cacheLease) {
|
|
628
630
|
this.adapter = adapter;
|
|
629
631
|
this.state = state;
|
|
@@ -735,9 +737,13 @@ class OpenAiResponsesTurnSession {
|
|
|
735
737
|
}
|
|
736
738
|
beginAttempt() {
|
|
737
739
|
this.#connectionDiagnostic = undefined;
|
|
740
|
+
this.#webSocketDiagnostic = undefined;
|
|
741
|
+
}
|
|
742
|
+
captureWebSocketDiagnostic(diagnostic) {
|
|
743
|
+
this.#webSocketDiagnostic = diagnostic;
|
|
738
744
|
}
|
|
739
745
|
connectionDiagnostic() {
|
|
740
|
-
return this.#connectionDiagnostic
|
|
746
|
+
return { ...this.#connectionDiagnostic, ...this.#webSocketDiagnostic };
|
|
741
747
|
}
|
|
742
748
|
captureTurnState(value) {
|
|
743
749
|
if (this.#turnState !== undefined || typeof value !== 'string')
|
|
@@ -1010,6 +1016,10 @@ async function* parseOpenAiResponsesSse(response, signal, continuation) {
|
|
|
1010
1016
|
}
|
|
1011
1017
|
}
|
|
1012
1018
|
async function* webSocketResponseEvents(input) {
|
|
1019
|
+
const body = input.turn.withTurnStateMetadata(input.body);
|
|
1020
|
+
const payload = Buffer.from(JSON.stringify({ type: 'response.create', ...body }), 'utf8');
|
|
1021
|
+
const diagnostic = webSocketRequestDiagnostic(body, payload.length);
|
|
1022
|
+
input.turn.captureWebSocketDiagnostic(diagnostic);
|
|
1013
1023
|
const socket = await input.turn.webSocketForRequest({
|
|
1014
1024
|
configuration: input.configuration,
|
|
1015
1025
|
promptCacheKey: input.promptCacheKey,
|
|
@@ -1018,32 +1028,75 @@ async function* webSocketResponseEvents(input) {
|
|
|
1018
1028
|
continuation: input.continuation,
|
|
1019
1029
|
credentialAcquired: input.credentialAcquired
|
|
1020
1030
|
});
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
type
|
|
1034
|
-
|
|
1035
|
-
|
|
1031
|
+
const messages = webSocketMessages(socket, input.signal, (code) => {
|
|
1032
|
+
diagnostic.webSocketCloseCode = code;
|
|
1033
|
+
});
|
|
1034
|
+
try {
|
|
1035
|
+
diagnostic.webSocketPhase = 'SEND';
|
|
1036
|
+
await sendWebSocketMessage(socket, payload, input.signal, () => {
|
|
1037
|
+
diagnostic.sentFrameCount = (diagnostic.sentFrameCount ?? 0) + 1;
|
|
1038
|
+
});
|
|
1039
|
+
diagnostic.webSocketPhase = 'RECEIVE';
|
|
1040
|
+
let terminal = false;
|
|
1041
|
+
for await (const item of messages) {
|
|
1042
|
+
const event = record(item);
|
|
1043
|
+
const type = text(event.type);
|
|
1044
|
+
input.turn.captureTurnState(turnStateFromEvent(event));
|
|
1045
|
+
if (type !== 'response.metadata' && type !== 'codex.response.metadata')
|
|
1046
|
+
yield item;
|
|
1047
|
+
if (type === 'response.completed' ||
|
|
1048
|
+
type === 'response.incomplete' ||
|
|
1049
|
+
type === 'response.failed' ||
|
|
1050
|
+
type === 'error') {
|
|
1051
|
+
terminal = true;
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1036
1054
|
}
|
|
1055
|
+
if (!terminal && !input.signal.aborted)
|
|
1056
|
+
throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'OpenAI Responses WebSocket closed before a terminal event.', {
|
|
1057
|
+
details: {
|
|
1058
|
+
reason: OPENAI_RESPONSES_EARLY_EOF_REASON,
|
|
1059
|
+
transport: 'WEBSOCKET',
|
|
1060
|
+
continuation: input.continuation
|
|
1061
|
+
},
|
|
1062
|
+
retryable: true
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
finally {
|
|
1066
|
+
messages.dispose();
|
|
1037
1067
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1068
|
+
}
|
|
1069
|
+
function webSocketRequestDiagnostic(body, requestBytes) {
|
|
1070
|
+
let imageCount = 0;
|
|
1071
|
+
let imageBytes = 0;
|
|
1072
|
+
for (const item of body.input) {
|
|
1073
|
+
const entry = record(item);
|
|
1074
|
+
const parts = Array.isArray(entry.content)
|
|
1075
|
+
? entry.content
|
|
1076
|
+
: Array.isArray(entry.output)
|
|
1077
|
+
? entry.output
|
|
1078
|
+
: [];
|
|
1079
|
+
for (const value of parts) {
|
|
1080
|
+
const part = record(value);
|
|
1081
|
+
if (part.type !== 'input_image')
|
|
1082
|
+
continue;
|
|
1083
|
+
imageCount++;
|
|
1084
|
+
const url = text(part.image_url);
|
|
1085
|
+
if (url === undefined)
|
|
1086
|
+
continue;
|
|
1087
|
+
const marker = url.indexOf(';base64,');
|
|
1088
|
+
if (url.startsWith('data:') && marker >= 0)
|
|
1089
|
+
imageBytes += Buffer.byteLength(url.slice(marker + 8), 'base64');
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
requestBytes,
|
|
1094
|
+
imageCount,
|
|
1095
|
+
imageBytes,
|
|
1096
|
+
requestFrameCount: Math.ceil(requestBytes / WEBSOCKET_SEND_FRAME_BYTES),
|
|
1097
|
+
sentFrameCount: 0,
|
|
1098
|
+
webSocketPhase: 'CONNECT'
|
|
1099
|
+
};
|
|
1047
1100
|
}
|
|
1048
1101
|
async function waitForWebSocketOpen(socket, signal) {
|
|
1049
1102
|
if (signal.aborted) {
|
|
@@ -1101,29 +1154,55 @@ async function waitForWebSocketOpen(socket, signal) {
|
|
|
1101
1154
|
socket.once('unexpected-response', unexpectedResponse);
|
|
1102
1155
|
});
|
|
1103
1156
|
}
|
|
1104
|
-
async function sendWebSocketMessage(socket,
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
cleanup()
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1157
|
+
async function sendWebSocketMessage(socket, payload, signal, frameSent) {
|
|
1158
|
+
for (let offset = 0; offset < payload.length; offset += WEBSOCKET_SEND_FRAME_BYTES) {
|
|
1159
|
+
if (signal.aborted)
|
|
1160
|
+
throw signal.reason;
|
|
1161
|
+
const end = Math.min(payload.length, offset + WEBSOCKET_SEND_FRAME_BYTES);
|
|
1162
|
+
await new Promise((resolve, reject) => {
|
|
1163
|
+
let settled = false;
|
|
1164
|
+
const cleanup = () => {
|
|
1165
|
+
settled = true;
|
|
1166
|
+
signal.removeEventListener('abort', abort);
|
|
1167
|
+
socket.off('error', error);
|
|
1168
|
+
socket.off('close', close);
|
|
1169
|
+
};
|
|
1170
|
+
const abort = () => {
|
|
1171
|
+
cleanup();
|
|
1172
|
+
socket.terminate();
|
|
1120
1173
|
reject(signal.reason);
|
|
1121
|
-
|
|
1122
|
-
|
|
1174
|
+
};
|
|
1175
|
+
const error = (cause) => {
|
|
1176
|
+
cleanup();
|
|
1177
|
+
reject(webSocketReceiveError(cause));
|
|
1178
|
+
};
|
|
1179
|
+
const close = () => error(new Error('WebSocket closed while sending the request.'));
|
|
1180
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
1181
|
+
socket.once('error', error);
|
|
1182
|
+
socket.once('close', close);
|
|
1183
|
+
try {
|
|
1184
|
+
socket.send(payload.subarray(offset, end), { binary: false, fin: end === payload.length }, (cause) => {
|
|
1185
|
+
if (settled)
|
|
1186
|
+
return;
|
|
1187
|
+
cleanup();
|
|
1188
|
+
if (cause)
|
|
1189
|
+
reject(webSocketReceiveError(cause));
|
|
1190
|
+
else if (signal.aborted)
|
|
1191
|
+
reject(signal.reason);
|
|
1192
|
+
else {
|
|
1193
|
+
frameSent();
|
|
1194
|
+
resolve();
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
catch (cause) {
|
|
1199
|
+
cleanup();
|
|
1200
|
+
reject(webSocketReceiveError(cause));
|
|
1201
|
+
}
|
|
1123
1202
|
});
|
|
1124
|
-
}
|
|
1203
|
+
}
|
|
1125
1204
|
}
|
|
1126
|
-
|
|
1205
|
+
function webSocketMessages(socket, signal, closed) {
|
|
1127
1206
|
const queue = [];
|
|
1128
1207
|
let resume;
|
|
1129
1208
|
const push = (item) => {
|
|
@@ -1136,7 +1215,10 @@ async function* webSocketMessages(socket, signal) {
|
|
|
1136
1215
|
value,
|
|
1137
1216
|
observedAt: performance.now()
|
|
1138
1217
|
});
|
|
1139
|
-
const close = () =>
|
|
1218
|
+
const close = (code) => {
|
|
1219
|
+
closed(code);
|
|
1220
|
+
push({ type: 'closed' });
|
|
1221
|
+
};
|
|
1140
1222
|
const error = (cause) => push({
|
|
1141
1223
|
type: 'error',
|
|
1142
1224
|
error: webSocketReceiveError(cause)
|
|
@@ -1153,49 +1235,57 @@ async function* webSocketMessages(socket, signal) {
|
|
|
1153
1235
|
socket.on('close', close);
|
|
1154
1236
|
socket.on('error', error);
|
|
1155
1237
|
signal.addEventListener('abort', abort, { once: true });
|
|
1156
|
-
|
|
1157
|
-
let idleDeadline = Date.now() + WEBSOCKET_IDLE_TIMEOUT_MS;
|
|
1158
|
-
while (true) {
|
|
1159
|
-
if (Date.now() >= idleDeadline)
|
|
1160
|
-
throw webSocketIdleError(socket);
|
|
1161
|
-
if (queue.length === 0)
|
|
1162
|
-
await new Promise((resolve, reject) => {
|
|
1163
|
-
const timer = setTimeout(() => {
|
|
1164
|
-
resume = undefined;
|
|
1165
|
-
reject(webSocketIdleError(socket));
|
|
1166
|
-
}, Math.max(0, idleDeadline - Date.now()));
|
|
1167
|
-
resume = () => {
|
|
1168
|
-
clearTimeout(timer);
|
|
1169
|
-
resolve();
|
|
1170
|
-
};
|
|
1171
|
-
});
|
|
1172
|
-
const item = queue.shift();
|
|
1173
|
-
if (item === undefined)
|
|
1174
|
-
continue;
|
|
1175
|
-
if (item.type === 'closed')
|
|
1176
|
-
return;
|
|
1177
|
-
if (item.type === 'error')
|
|
1178
|
-
throw item.error;
|
|
1179
|
-
const text = await webSocketMessageText(item.value);
|
|
1180
|
-
if (Buffer.byteLength(text) > 16 * 1024 * 1024)
|
|
1181
|
-
throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Model response exceeded the size limit.');
|
|
1182
|
-
try {
|
|
1183
|
-
const event = JSON.parse(text);
|
|
1184
|
-
if (!isWebSocketNonProgressEvent(event))
|
|
1185
|
-
idleDeadline = Date.now() + WEBSOCKET_IDLE_TIMEOUT_MS;
|
|
1186
|
-
yield observeModelStreamEvent(event, item.observedAt);
|
|
1187
|
-
}
|
|
1188
|
-
catch (cause) {
|
|
1189
|
-
throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Model WebSocket contained invalid JSON.', { cause });
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
finally {
|
|
1238
|
+
const dispose = () => {
|
|
1194
1239
|
signal.removeEventListener('abort', abort);
|
|
1195
1240
|
socket.off('message', message);
|
|
1196
1241
|
socket.off('close', close);
|
|
1197
1242
|
socket.off('error', error);
|
|
1198
|
-
}
|
|
1243
|
+
};
|
|
1244
|
+
return {
|
|
1245
|
+
dispose,
|
|
1246
|
+
async *[Symbol.asyncIterator]() {
|
|
1247
|
+
try {
|
|
1248
|
+
let idleDeadline = Date.now() + WEBSOCKET_IDLE_TIMEOUT_MS;
|
|
1249
|
+
while (true) {
|
|
1250
|
+
if (Date.now() >= idleDeadline)
|
|
1251
|
+
throw webSocketIdleError(socket);
|
|
1252
|
+
if (queue.length === 0)
|
|
1253
|
+
await new Promise((resolve, reject) => {
|
|
1254
|
+
const timer = setTimeout(() => {
|
|
1255
|
+
resume = undefined;
|
|
1256
|
+
reject(webSocketIdleError(socket));
|
|
1257
|
+
}, Math.max(0, idleDeadline - Date.now()));
|
|
1258
|
+
resume = () => {
|
|
1259
|
+
clearTimeout(timer);
|
|
1260
|
+
resolve();
|
|
1261
|
+
};
|
|
1262
|
+
});
|
|
1263
|
+
const item = queue.shift();
|
|
1264
|
+
if (item === undefined)
|
|
1265
|
+
continue;
|
|
1266
|
+
if (item.type === 'closed')
|
|
1267
|
+
return;
|
|
1268
|
+
if (item.type === 'error')
|
|
1269
|
+
throw item.error;
|
|
1270
|
+
const text = await webSocketMessageText(item.value);
|
|
1271
|
+
if (Buffer.byteLength(text) > 16 * 1024 * 1024)
|
|
1272
|
+
throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Model response exceeded the size limit.');
|
|
1273
|
+
try {
|
|
1274
|
+
const event = JSON.parse(text);
|
|
1275
|
+
if (!isWebSocketNonProgressEvent(event))
|
|
1276
|
+
idleDeadline = Date.now() + WEBSOCKET_IDLE_TIMEOUT_MS;
|
|
1277
|
+
yield observeModelStreamEvent(event, item.observedAt);
|
|
1278
|
+
}
|
|
1279
|
+
catch (cause) {
|
|
1280
|
+
throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Model WebSocket contained invalid JSON.', { cause });
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
finally {
|
|
1285
|
+
dispose();
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1199
1289
|
}
|
|
1200
1290
|
function webSocketIdleError(socket) {
|
|
1201
1291
|
terminateWebSocket(socket);
|
|
@@ -4,6 +4,8 @@ import type { SessionListInput, SessionSource } from './types.js';
|
|
|
4
4
|
export interface MarAgentSessionSummary {
|
|
5
5
|
id: string;
|
|
6
6
|
title: string;
|
|
7
|
+
/** First non-empty user input, whitespace-normalized and bounded to 160 characters. */
|
|
8
|
+
firstUserMessagePreview?: string;
|
|
7
9
|
workspace: string;
|
|
8
10
|
createdAt: string;
|
|
9
11
|
lastActivityAt: string;
|
|
@@ -54,6 +56,7 @@ export interface MarAgentSessionImage {
|
|
|
54
56
|
/** Public session catalog facade. Storage details remain SDK-private. */
|
|
55
57
|
export interface MarAgentSessionCatalog {
|
|
56
58
|
list(input?: SessionListInput): Promise<MarAgentSessionPage>;
|
|
59
|
+
firstUserMessagePreview(sessionId: string): Promise<string | undefined>;
|
|
57
60
|
executionConfiguration(sessionId: string): Promise<{
|
|
58
61
|
readonly modelId: string;
|
|
59
62
|
readonly modelName?: string;
|
|
@@ -7,6 +7,7 @@ export async function openMarAgentSessionCatalog(input) {
|
|
|
7
7
|
const store = await JsonlSessionStore.open(input.homeDirectory);
|
|
8
8
|
return {
|
|
9
9
|
list: (options) => store.list(options),
|
|
10
|
+
firstUserMessagePreview: (sessionId) => store.firstUserMessagePreview(sessionId),
|
|
10
11
|
executionConfiguration: (sessionId) => store.executionConfiguration(sessionId),
|
|
11
12
|
history: async (sessionId, options = {}) => {
|
|
12
13
|
const beforeSequence = parseCursor(options.cursor);
|
|
@@ -64,6 +64,8 @@ interface SessionMeta {
|
|
|
64
64
|
todoSequence?: number;
|
|
65
65
|
todoRevision: number;
|
|
66
66
|
executionModelIndexed?: true;
|
|
67
|
+
firstUserMessageIndexed?: true;
|
|
68
|
+
firstUserMessagePreview?: string;
|
|
67
69
|
modelId?: string;
|
|
68
70
|
modelName?: string;
|
|
69
71
|
reasoningEffort?: string;
|
|
@@ -184,12 +186,13 @@ export declare class JsonlSessionStore {
|
|
|
184
186
|
reasoningEffort?: string;
|
|
185
187
|
modelName?: string;
|
|
186
188
|
modelId?: string;
|
|
187
|
-
id: string;
|
|
188
|
-
title: string;
|
|
189
189
|
workspace: string;
|
|
190
190
|
createdAt: string;
|
|
191
191
|
lastActivityAt: string;
|
|
192
192
|
source: SessionSource;
|
|
193
|
+
firstUserMessagePreview?: string;
|
|
194
|
+
id: string;
|
|
195
|
+
title: string;
|
|
193
196
|
}[];
|
|
194
197
|
}>;
|
|
195
198
|
listSubagentChildren(parentSessionId: string): Promise<{
|
|
@@ -216,6 +219,7 @@ export declare class JsonlSessionStore {
|
|
|
216
219
|
readonly reasoningEffort?: string;
|
|
217
220
|
} | undefined>;
|
|
218
221
|
sessionTree(sessionId: string): Promise<readonly string[]>;
|
|
222
|
+
firstUserMessagePreview(sessionId: string): Promise<string | undefined>;
|
|
219
223
|
delete(sessionId: string): Promise<{
|
|
220
224
|
readonly deleted: boolean;
|
|
221
225
|
}>;
|
|
@@ -158,6 +158,7 @@ export class JsonlSessionStore {
|
|
|
158
158
|
closed: false,
|
|
159
159
|
indexDirty: false,
|
|
160
160
|
dirtySessionVersions: new Map(),
|
|
161
|
+
previewOnlyDirtySessions: new Set(),
|
|
161
162
|
pendingPublishIds: new Set(),
|
|
162
163
|
codeStateWrites: new Map(),
|
|
163
164
|
recordWrites: new Map(),
|
|
@@ -176,19 +177,26 @@ export class JsonlSessionStore {
|
|
|
176
177
|
if (!disk)
|
|
177
178
|
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session index is missing.');
|
|
178
179
|
const dirty = new Map(this.#shared.dirtySessionVersions);
|
|
180
|
+
const previewOnly = new Set(this.#shared.previewOnlyDirtySessions);
|
|
179
181
|
const sessions = new Map(disk.sessions.map((session) => [session.id, session]));
|
|
180
182
|
const created = new Set(createdIds);
|
|
181
183
|
for (const session of this.#shared.index.sessions) {
|
|
182
184
|
const current = sessions.get(session.id);
|
|
183
185
|
// Disk membership is authoritative: a stale in-memory snapshot cannot revive a deletion.
|
|
184
186
|
if (created.has(session.id) || (current && dirty.has(session.id)))
|
|
185
|
-
sessions.set(session.id,
|
|
187
|
+
sessions.set(session.id, current && previewOnly.has(session.id)
|
|
188
|
+
? mergeFirstUserMessagePreviewCache(current, session)
|
|
189
|
+
: structuredClone(session));
|
|
186
190
|
}
|
|
187
191
|
for (const deletedId of typeof deletedIds === 'string' ? [deletedIds] : (deletedIds ?? []))
|
|
188
192
|
sessions.delete(deletedId);
|
|
189
193
|
const updated = {
|
|
190
194
|
version: 1,
|
|
191
|
-
revision:
|
|
195
|
+
revision: createdIds.length > 0 ||
|
|
196
|
+
deletedIds !== undefined ||
|
|
197
|
+
[...dirty.keys()].some((id) => !previewOnly.has(id))
|
|
198
|
+
? Math.max(disk.revision, this.#shared.index.revision) + 1
|
|
199
|
+
: disk.revision,
|
|
192
200
|
sessions: [...sessions.values()]
|
|
193
201
|
};
|
|
194
202
|
await persistSessionIndex(path, updated);
|
|
@@ -196,8 +204,10 @@ export class JsonlSessionStore {
|
|
|
196
204
|
// A different creation still needs its own index transaction to publish its directory.
|
|
197
205
|
if (this.#shared.pendingPublishIds.has(id) && !created.has(id))
|
|
198
206
|
continue;
|
|
199
|
-
if (this.#shared.dirtySessionVersions.get(id) === version)
|
|
207
|
+
if (this.#shared.dirtySessionVersions.get(id) === version) {
|
|
200
208
|
this.#shared.dirtySessionVersions.delete(id);
|
|
209
|
+
this.#shared.previewOnlyDirtySessions.delete(id);
|
|
210
|
+
}
|
|
201
211
|
}
|
|
202
212
|
for (const id of created)
|
|
203
213
|
this.#shared.pendingPublishIds.delete(id);
|
|
@@ -234,7 +244,11 @@ export class JsonlSessionStore {
|
|
|
234
244
|
sessions: disk.sessions
|
|
235
245
|
.map((session) => {
|
|
236
246
|
const current = local.get(session.id);
|
|
237
|
-
|
|
247
|
+
if (!current || !this.#shared.dirtySessionVersions.has(session.id))
|
|
248
|
+
return session;
|
|
249
|
+
return this.#shared.previewOnlyDirtySessions.has(session.id)
|
|
250
|
+
? mergeFirstUserMessagePreviewCache(session, current)
|
|
251
|
+
: current;
|
|
238
252
|
})
|
|
239
253
|
.concat(this.#shared.index.sessions.filter((session) => this.#shared.pendingPublishIds.has(session.id) && !published.has(session.id)))
|
|
240
254
|
};
|
|
@@ -272,7 +286,12 @@ export class JsonlSessionStore {
|
|
|
272
286
|
source: normalizeSessionSource(input.source),
|
|
273
287
|
nextSequence: 1,
|
|
274
288
|
automaticTitlePending: input.title === undefined,
|
|
275
|
-
resume: {
|
|
289
|
+
resume: {
|
|
290
|
+
version: 1,
|
|
291
|
+
todoRevision: 0,
|
|
292
|
+
executionModelIndexed: true,
|
|
293
|
+
firstUserMessageIndexed: true
|
|
294
|
+
},
|
|
276
295
|
segments: []
|
|
277
296
|
};
|
|
278
297
|
const publication = await this.#beginUnpublishedSession(id);
|
|
@@ -520,7 +539,12 @@ export class JsonlSessionStore {
|
|
|
520
539
|
source: { type: 'user' },
|
|
521
540
|
nextSequence: 1,
|
|
522
541
|
automaticTitlePending: false,
|
|
523
|
-
resume: {
|
|
542
|
+
resume: {
|
|
543
|
+
version: 1,
|
|
544
|
+
todoRevision: 0,
|
|
545
|
+
executionModelIndexed: true,
|
|
546
|
+
firstUserMessageIndexed: true
|
|
547
|
+
},
|
|
524
548
|
segments: []
|
|
525
549
|
};
|
|
526
550
|
const publication = await this.#beginUnpublishedSession(target.id);
|
|
@@ -852,7 +876,7 @@ export class JsonlSessionStore {
|
|
|
852
876
|
meta.lastActivityAt = clearedAt;
|
|
853
877
|
meta.nextSequence = 2;
|
|
854
878
|
meta.storageTransactionId = transactionId;
|
|
855
|
-
meta.resume = { version: 1, todoRevision: 0 };
|
|
879
|
+
meta.resume = { version: 1, todoRevision: 0, firstUserMessageIndexed: true };
|
|
856
880
|
meta.segments = staged.segments;
|
|
857
881
|
this.#shared.segments.set(sessionId, 1);
|
|
858
882
|
this.#shared.index.revision++;
|
|
@@ -1316,6 +1340,9 @@ export class JsonlSessionStore {
|
|
|
1316
1340
|
const sessions = ordered.slice(start, start + limit).map((session) => ({
|
|
1317
1341
|
id: session.id,
|
|
1318
1342
|
title: session.title,
|
|
1343
|
+
...(session.resume?.firstUserMessagePreview === undefined
|
|
1344
|
+
? {}
|
|
1345
|
+
: { firstUserMessagePreview: session.resume.firstUserMessagePreview }),
|
|
1319
1346
|
workspace: session.workspace,
|
|
1320
1347
|
createdAt: session.createdAt,
|
|
1321
1348
|
lastActivityAt: session.lastActivityAt,
|
|
@@ -1403,6 +1430,11 @@ export class JsonlSessionStore {
|
|
|
1403
1430
|
const resume = meta.resume;
|
|
1404
1431
|
if (resume?.version !== 1 || !Number.isSafeInteger(resume.todoRevision))
|
|
1405
1432
|
return;
|
|
1433
|
+
if (resume.firstUserMessageIndexed === true && resume.firstUserMessagePreview === undefined) {
|
|
1434
|
+
const preview = userMessagePreviewFromRecord(record);
|
|
1435
|
+
if (preview !== undefined)
|
|
1436
|
+
resume.firstUserMessagePreview = preview;
|
|
1437
|
+
}
|
|
1406
1438
|
const executionModel = executionModelFromRecord(record);
|
|
1407
1439
|
if (executionModel !== undefined) {
|
|
1408
1440
|
resume.executionModelIndexed = true;
|
|
@@ -1488,6 +1520,56 @@ export class JsonlSessionStore {
|
|
|
1488
1520
|
contextRecords: state.contextRecords
|
|
1489
1521
|
};
|
|
1490
1522
|
}
|
|
1523
|
+
async firstUserMessagePreview(sessionId) {
|
|
1524
|
+
this.#assertOpen();
|
|
1525
|
+
await this.#refreshIndex();
|
|
1526
|
+
await this.#ensureFirstUserMessageIndexed(sessionId);
|
|
1527
|
+
return this.#shared.index.sessions.find((session) => session.id === sessionId)?.resume
|
|
1528
|
+
?.firstUserMessagePreview;
|
|
1529
|
+
}
|
|
1530
|
+
async #ensureFirstUserMessageIndexed(sessionId) {
|
|
1531
|
+
for (;;) {
|
|
1532
|
+
const before = this.#shared.index.sessions.find((session) => session.id === sessionId);
|
|
1533
|
+
if (before === undefined)
|
|
1534
|
+
throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
|
|
1535
|
+
if (before.resume?.firstUserMessageIndexed === true)
|
|
1536
|
+
return;
|
|
1537
|
+
const nextSequence = before.nextSequence;
|
|
1538
|
+
const transactionId = before.storageTransactionId;
|
|
1539
|
+
let preview;
|
|
1540
|
+
try {
|
|
1541
|
+
for (const path of await this.#segmentPaths(sessionId)) {
|
|
1542
|
+
for await (const record of readRecordsFromHead(path)) {
|
|
1543
|
+
preview = userMessagePreviewFromRecord(record);
|
|
1544
|
+
if (preview !== undefined)
|
|
1545
|
+
break;
|
|
1546
|
+
}
|
|
1547
|
+
if (preview !== undefined)
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
catch (error) {
|
|
1552
|
+
if (error.code !== 'ENOENT' &&
|
|
1553
|
+
!(error instanceof MarAgentError && error.code === 'MAR_AGENT_SESSION_NOT_FOUND'))
|
|
1554
|
+
throw error;
|
|
1555
|
+
}
|
|
1556
|
+
await this.#refreshIndex();
|
|
1557
|
+
const current = this.#shared.index.sessions.find((session) => session.id === sessionId);
|
|
1558
|
+
if (current === undefined)
|
|
1559
|
+
throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
|
|
1560
|
+
if (current.resume?.firstUserMessageIndexed === true)
|
|
1561
|
+
return;
|
|
1562
|
+
if (current.nextSequence !== nextSequence || current.storageTransactionId !== transactionId)
|
|
1563
|
+
continue;
|
|
1564
|
+
const resume = (current.resume ??= { version: 1, todoRevision: 0 });
|
|
1565
|
+
resume.firstUserMessageIndexed = true;
|
|
1566
|
+
if (preview !== undefined)
|
|
1567
|
+
resume.firstUserMessagePreview = preview;
|
|
1568
|
+
// Derived preview caches do not change list membership, ordering or filters.
|
|
1569
|
+
this.#markIndexDirty(sessionId, true);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1491
1573
|
async #ensureExecutionModelIndexed(sessionId) {
|
|
1492
1574
|
const pending = this.#shared.executionModelReads.get(sessionId);
|
|
1493
1575
|
if (pending !== undefined)
|
|
@@ -1586,7 +1668,11 @@ export class JsonlSessionStore {
|
|
|
1586
1668
|
if (this.#leaseClosed || this.#shared.closed)
|
|
1587
1669
|
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Session store is closed.');
|
|
1588
1670
|
}
|
|
1589
|
-
#markIndexDirty(sessionId) {
|
|
1671
|
+
#markIndexDirty(sessionId, previewOnly = false) {
|
|
1672
|
+
if (!previewOnly)
|
|
1673
|
+
this.#shared.previewOnlyDirtySessions.delete(sessionId);
|
|
1674
|
+
else if (!this.#shared.dirtySessionVersions.has(sessionId))
|
|
1675
|
+
this.#shared.previewOnlyDirtySessions.add(sessionId);
|
|
1590
1676
|
this.#shared.indexDirty = true;
|
|
1591
1677
|
this.#shared.dirtySessionVersions.set(sessionId, (this.#shared.dirtySessionVersions.get(sessionId) ?? 0) + 1);
|
|
1592
1678
|
}
|
|
@@ -1679,6 +1765,26 @@ export class JsonlSessionStore {
|
|
|
1679
1765
|
JsonlSessionStore.#instances.delete(this.#shared.root);
|
|
1680
1766
|
}
|
|
1681
1767
|
}
|
|
1768
|
+
/** Merge a derived cache only onto the unchanged history it was computed from. */
|
|
1769
|
+
function mergeFirstUserMessagePreviewCache(current, cached) {
|
|
1770
|
+
if (cached.resume?.firstUserMessageIndexed !== true ||
|
|
1771
|
+
current.nextSequence !== cached.nextSequence ||
|
|
1772
|
+
current.storageTransactionId !== cached.storageTransactionId ||
|
|
1773
|
+
current.resume?.firstUserMessagePreview !== undefined ||
|
|
1774
|
+
(current.resume?.firstUserMessageIndexed === true &&
|
|
1775
|
+
cached.resume.firstUserMessagePreview === undefined))
|
|
1776
|
+
return current;
|
|
1777
|
+
return {
|
|
1778
|
+
...current,
|
|
1779
|
+
resume: {
|
|
1780
|
+
...(current.resume ?? { version: 1, todoRevision: 0 }),
|
|
1781
|
+
firstUserMessageIndexed: true,
|
|
1782
|
+
...(cached.resume?.firstUserMessagePreview === undefined
|
|
1783
|
+
? {}
|
|
1784
|
+
: { firstUserMessagePreview: cached.resume.firstUserMessagePreview })
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1682
1788
|
async function readSessionIndex(path) {
|
|
1683
1789
|
let raw;
|
|
1684
1790
|
try {
|
|
@@ -2157,7 +2263,9 @@ function deriveExecutionState(records) {
|
|
|
2157
2263
|
let todoRevision = 0;
|
|
2158
2264
|
let todoItems = [];
|
|
2159
2265
|
let executionModel;
|
|
2266
|
+
let firstUserMessagePreview;
|
|
2160
2267
|
for (const record of records) {
|
|
2268
|
+
firstUserMessagePreview ??= userMessagePreviewFromRecord(record);
|
|
2161
2269
|
if (record.type === 'session.created')
|
|
2162
2270
|
automaticTitle =
|
|
2163
2271
|
record.payload?.automaticTitle === true;
|
|
@@ -2198,10 +2306,21 @@ function deriveExecutionState(records) {
|
|
|
2198
2306
|
...(todoSequence === undefined ? {} : { todoSequence }),
|
|
2199
2307
|
todoRevision,
|
|
2200
2308
|
executionModelIndexed: true,
|
|
2309
|
+
firstUserMessageIndexed: true,
|
|
2310
|
+
...(firstUserMessagePreview === undefined ? {} : { firstUserMessagePreview }),
|
|
2201
2311
|
...(executionModel === undefined ? {} : executionModel)
|
|
2202
2312
|
}
|
|
2203
2313
|
};
|
|
2204
2314
|
}
|
|
2315
|
+
function userMessagePreviewFromRecord(record) {
|
|
2316
|
+
if (record.type !== 'turn.user' ||
|
|
2317
|
+
!isObject(record.payload) ||
|
|
2318
|
+
record.payload.inputSource === 'parent_agent' ||
|
|
2319
|
+
typeof record.payload.prompt !== 'string')
|
|
2320
|
+
return undefined;
|
|
2321
|
+
const preview = record.payload.prompt.trim().replace(/\s+/gu, ' ').slice(0, 160);
|
|
2322
|
+
return preview || undefined;
|
|
2323
|
+
}
|
|
2205
2324
|
function executionModelFromRecord(record) {
|
|
2206
2325
|
if (record.type !== 'execution.header' || !isObject(record.payload))
|
|
2207
2326
|
return undefined;
|
|
@@ -2254,10 +2373,63 @@ function sameResumeIndex(left, right) {
|
|
|
2254
2373
|
left.todoSequence === right.todoSequence &&
|
|
2255
2374
|
left.todoRevision === right.todoRevision &&
|
|
2256
2375
|
left.executionModelIndexed === right.executionModelIndexed &&
|
|
2376
|
+
left.firstUserMessageIndexed === right.firstUserMessageIndexed &&
|
|
2377
|
+
left.firstUserMessagePreview === right.firstUserMessagePreview &&
|
|
2257
2378
|
left.modelId === right.modelId &&
|
|
2258
2379
|
left.modelName === right.modelName &&
|
|
2259
2380
|
left.reasoningEffort === right.reasoningEffort);
|
|
2260
2381
|
}
|
|
2382
|
+
/** Read only the prefix needed by discovery; never load the entire transcript. */
|
|
2383
|
+
async function* readRecordsFromHead(path) {
|
|
2384
|
+
const handle = await open(path, 'r');
|
|
2385
|
+
try {
|
|
2386
|
+
const { size } = await handle.stat();
|
|
2387
|
+
const chunk = Buffer.allocUnsafe(REVERSE_READ_CHUNK_BYTES);
|
|
2388
|
+
let position = 0;
|
|
2389
|
+
let pending = Buffer.alloc(0);
|
|
2390
|
+
while (position < size) {
|
|
2391
|
+
const { bytesRead } = await handle.read(chunk, 0, Math.min(chunk.length, size - position), position);
|
|
2392
|
+
if (bytesRead === 0)
|
|
2393
|
+
break;
|
|
2394
|
+
position += bytesRead;
|
|
2395
|
+
const data = Buffer.concat([pending, chunk.subarray(0, bytesRead)]);
|
|
2396
|
+
let start = 0;
|
|
2397
|
+
for (;;) {
|
|
2398
|
+
const end = data.indexOf(0x0a, start);
|
|
2399
|
+
if (end < 0)
|
|
2400
|
+
break;
|
|
2401
|
+
const line = data.subarray(start, end).toString('utf8');
|
|
2402
|
+
start = end + 1;
|
|
2403
|
+
if (!line)
|
|
2404
|
+
continue;
|
|
2405
|
+
let record;
|
|
2406
|
+
try {
|
|
2407
|
+
record = JSON.parse(line);
|
|
2408
|
+
}
|
|
2409
|
+
catch (cause) {
|
|
2410
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.', {
|
|
2411
|
+
cause
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
yield record;
|
|
2415
|
+
}
|
|
2416
|
+
pending = Buffer.from(data.subarray(start));
|
|
2417
|
+
}
|
|
2418
|
+
if (pending.length > 0) {
|
|
2419
|
+
let record;
|
|
2420
|
+
try {
|
|
2421
|
+
record = JSON.parse(pending.toString('utf8'));
|
|
2422
|
+
}
|
|
2423
|
+
catch {
|
|
2424
|
+
return; // Readers ignore only an interrupted final line.
|
|
2425
|
+
}
|
|
2426
|
+
yield record;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
finally {
|
|
2430
|
+
await handle.close();
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2261
2433
|
async function readRecords(path) {
|
|
2262
2434
|
const value = await readFile(path, 'utf8');
|
|
2263
2435
|
const lines = value.split('\n');
|
|
@@ -13,6 +13,18 @@ export async function createCodeModeEngine(options) {
|
|
|
13
13
|
if (disposed || runtime)
|
|
14
14
|
throw diagnostic('MAR_AGENT_CODE_FAILED', 'Engine is not reusable.');
|
|
15
15
|
runtime = quickjs.newRuntime();
|
|
16
|
+
// QuickJS 0.32.0 caches the job's context-pointer view across WASM memory growth
|
|
17
|
+
// (upstream #240). A detached view creates an unowned Context and breaks disposal.
|
|
18
|
+
// Keep this dependency-specific correction inside the worker engine; preserve its
|
|
19
|
+
// pointer allocation/free lifecycle and always read from the current heap.
|
|
20
|
+
const newMutablePointerArray = runtime.memory.newMutablePointerArray.bind(runtime.memory);
|
|
21
|
+
runtime.memory.newMutablePointerArray = (length) => {
|
|
22
|
+
const pointer = newMutablePointerArray(length);
|
|
23
|
+
Object.defineProperty(pointer.value, 'typedArray', {
|
|
24
|
+
get: () => new Int32Array(runtime.module.HEAP8.buffer, pointer.value.ptr, length)
|
|
25
|
+
});
|
|
26
|
+
return pointer;
|
|
27
|
+
};
|
|
16
28
|
runtime.setMemoryLimit(input.limits.memoryBytes);
|
|
17
29
|
runtime.setMaxStackSize(input.limits.stackBytes);
|
|
18
30
|
let executionMs = 0;
|
|
@@ -27,17 +27,25 @@ export class ViewImageTool {
|
|
|
27
27
|
const path = await this.paths.resolve(input.path);
|
|
28
28
|
const handle = await open(path, 'r');
|
|
29
29
|
try {
|
|
30
|
-
const stat = await handle.stat();
|
|
30
|
+
const stat = await handle.stat({ bigint: true });
|
|
31
31
|
if (!stat.isFile())
|
|
32
32
|
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Path is not a regular file.');
|
|
33
|
-
if (stat.size > TOOL_EXECUTION_LIMITS.viewImageMaxBytes)
|
|
33
|
+
if (stat.size > BigInt(TOOL_EXECUTION_LIMITS.viewImageMaxBytes))
|
|
34
34
|
throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', `Image exceeds the ${TOOL_EXECUTION_LIMITS.viewImageMaxBytes}-byte limit.`);
|
|
35
35
|
signal?.throwIfAborted();
|
|
36
36
|
const data = await handle.readFile();
|
|
37
37
|
signal?.throwIfAborted();
|
|
38
38
|
if (data.byteLength > TOOL_EXECUTION_LIMITS.viewImageMaxBytes)
|
|
39
39
|
throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', `Image exceeds the ${TOOL_EXECUTION_LIMITS.viewImageMaxBytes}-byte limit.`);
|
|
40
|
+
const after = await handle.stat({ bigint: true });
|
|
41
|
+
signal?.throwIfAborted();
|
|
42
|
+
if (stat.size !== after.size ||
|
|
43
|
+
after.size !== BigInt(data.byteLength) ||
|
|
44
|
+
stat.mtimeNs !== after.mtimeNs ||
|
|
45
|
+
stat.ctimeNs !== after.ctimeNs)
|
|
46
|
+
return incompleteImage();
|
|
40
47
|
const mimeType = detectImageMime(data);
|
|
48
|
+
validateImageContainer(data, mimeType);
|
|
41
49
|
const metadata = {
|
|
42
50
|
path: input.path,
|
|
43
51
|
mimeType,
|
|
@@ -80,3 +88,101 @@ export function detectImageMime(data) {
|
|
|
80
88
|
function invalidInput() {
|
|
81
89
|
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'View image input is invalid.');
|
|
82
90
|
}
|
|
91
|
+
// Basic container checks catch partial writes without decoding or changing pixels.
|
|
92
|
+
function validateImageContainer(data, mimeType) {
|
|
93
|
+
if (mimeType === 'image/jpeg') {
|
|
94
|
+
let offset = 2;
|
|
95
|
+
let inScan = false;
|
|
96
|
+
let hasScan = false;
|
|
97
|
+
while (offset < data.length) {
|
|
98
|
+
if (data[offset++] !== 0xff) {
|
|
99
|
+
if (!inScan)
|
|
100
|
+
incompleteImage();
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
while (data[offset] === 0xff)
|
|
104
|
+
offset++;
|
|
105
|
+
if (offset >= data.length)
|
|
106
|
+
incompleteImage();
|
|
107
|
+
const marker = data[offset++];
|
|
108
|
+
if (marker === 0 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
109
|
+
if (!inScan)
|
|
110
|
+
incompleteImage();
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (marker === 0xd9) {
|
|
114
|
+
if (!hasScan)
|
|
115
|
+
incompleteImage();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (marker === 0xd8)
|
|
119
|
+
incompleteImage();
|
|
120
|
+
if (marker === 1)
|
|
121
|
+
continue;
|
|
122
|
+
if (data.length - offset < 2)
|
|
123
|
+
incompleteImage();
|
|
124
|
+
const length = data.readUInt16BE(offset);
|
|
125
|
+
if (length < 2 || offset + length > data.length)
|
|
126
|
+
incompleteImage();
|
|
127
|
+
offset += length;
|
|
128
|
+
if (marker === 0xda)
|
|
129
|
+
hasScan = true;
|
|
130
|
+
inScan = marker === 0xda || (inScan && marker === 0xdc);
|
|
131
|
+
}
|
|
132
|
+
incompleteImage();
|
|
133
|
+
}
|
|
134
|
+
if (mimeType === 'image/webp') {
|
|
135
|
+
const end = data.readUInt32LE(4) + 8;
|
|
136
|
+
if (end > data.length || end < 20)
|
|
137
|
+
incompleteImage();
|
|
138
|
+
let offset = 12;
|
|
139
|
+
let hasPixels = false;
|
|
140
|
+
while (offset < end) {
|
|
141
|
+
if (end - offset < 8)
|
|
142
|
+
incompleteImage();
|
|
143
|
+
const length = data.readUInt32LE(offset + 4);
|
|
144
|
+
const next = offset + 8 + length + (length % 2);
|
|
145
|
+
if (next > end)
|
|
146
|
+
incompleteImage();
|
|
147
|
+
const type = data.toString('ascii', offset, offset + 4);
|
|
148
|
+
if ((type === 'VP8 ' || type === 'VP8L' || type === 'ANMF') && length > 0)
|
|
149
|
+
hasPixels = true;
|
|
150
|
+
offset = next;
|
|
151
|
+
}
|
|
152
|
+
if (!hasPixels)
|
|
153
|
+
incompleteImage();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
let offset = 8;
|
|
157
|
+
let hasPixels = false;
|
|
158
|
+
while (offset < data.length) {
|
|
159
|
+
if (data.length - offset < 12)
|
|
160
|
+
incompleteImage();
|
|
161
|
+
const length = data.readUInt32BE(offset);
|
|
162
|
+
const next = offset + 12 + length;
|
|
163
|
+
if (next > data.length)
|
|
164
|
+
incompleteImage();
|
|
165
|
+
const type = data.toString('ascii', offset + 4, offset + 8);
|
|
166
|
+
if (offset === 8) {
|
|
167
|
+
if (type !== 'IHDR' ||
|
|
168
|
+
length !== 13 ||
|
|
169
|
+
data.readUInt32BE(offset + 8) === 0 ||
|
|
170
|
+
data.readUInt32BE(offset + 12) === 0)
|
|
171
|
+
incompleteImage();
|
|
172
|
+
}
|
|
173
|
+
else if (type === 'IHDR')
|
|
174
|
+
incompleteImage();
|
|
175
|
+
if (type === 'IDAT' && length > 0)
|
|
176
|
+
hasPixels = true;
|
|
177
|
+
if (type === 'IEND') {
|
|
178
|
+
if (length !== 0 || !hasPixels)
|
|
179
|
+
incompleteImage();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
offset = next;
|
|
183
|
+
}
|
|
184
|
+
incompleteImage();
|
|
185
|
+
}
|
|
186
|
+
function incompleteImage() {
|
|
187
|
+
throw new MarAgentError('MAR_AGENT_IMAGE_INCOMPLETE', 'Image file is incomplete, malformed, or changed while reading. Wait for the writer to finish and read it again.');
|
|
188
|
+
}
|