@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.
@@ -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
- connectionDiagnostic(): Pick<ModelAttemptDiagnostic, 'connectionId' | 'connectionReused' | 'proxyType'>;
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 body = input.turn.withTurnStateMetadata(input.body);
1022
- await sendWebSocketMessage(socket, JSON.stringify({ type: 'response.create', ...body }), input.signal);
1023
- let terminal = false;
1024
- for await (const item of webSocketMessages(socket, input.signal)) {
1025
- const event = record(item);
1026
- const type = text(event.type);
1027
- input.turn.captureTurnState(turnStateFromEvent(event));
1028
- if (type !== 'response.metadata' && type !== 'codex.response.metadata')
1029
- yield item;
1030
- if (type === 'response.completed' ||
1031
- type === 'response.incomplete' ||
1032
- type === 'response.failed' ||
1033
- type === 'error') {
1034
- terminal = true;
1035
- return;
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
- if (!terminal && !input.signal.aborted)
1039
- throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'OpenAI Responses WebSocket closed before a terminal event.', {
1040
- details: {
1041
- reason: OPENAI_RESPONSES_EARLY_EOF_REASON,
1042
- transport: 'WEBSOCKET',
1043
- continuation: input.continuation
1044
- },
1045
- retryable: true
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, value, signal) {
1105
- if (signal.aborted)
1106
- throw signal.reason;
1107
- await new Promise((resolve, reject) => {
1108
- const cleanup = () => signal.removeEventListener('abort', abort);
1109
- const abort = () => {
1110
- socket.terminate();
1111
- cleanup();
1112
- reject(signal.reason);
1113
- };
1114
- signal.addEventListener('abort', abort, { once: true });
1115
- socket.send(value, (error) => {
1116
- cleanup();
1117
- if (error)
1118
- reject(webSocketReceiveError(error));
1119
- else if (signal.aborted)
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
- else
1122
- resolve();
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
- async function* webSocketMessages(socket, signal) {
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 = () => push({ type: 'closed' });
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
- try {
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, structuredClone(session));
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: Math.max(disk.revision, this.#shared.index.revision) + 1,
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
- return current && this.#shared.dirtySessionVersions.has(session.id) ? current : session;
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: { version: 1, todoRevision: 0, executionModelIndexed: true },
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: { version: 1, todoRevision: 0, executionModelIndexed: true },
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.9.157",
3
+ "version": "0.9.159",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",