@myagentroam/agent 0.11.24 → 0.11.25

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.
@@ -27,5 +27,6 @@ export declare class ResourceInputProjection {
27
27
  /** Propose a cursor; only the unified GC checkpoint may adopt it. */
28
28
  gcCandidate(request?: ModelRequest, mirroredImages?: readonly ModelImage[]): Promise<InlineMediaGcCandidate | undefined>;
29
29
  adoptGc(state: InlineMediaState | undefined): void;
30
+ releaseBytes(): void;
30
31
  }
31
32
  export {};
@@ -3,9 +3,11 @@ import { MarAgentError } from '../error.js';
3
3
  import { isModelImageUrl } from '../model/image.js';
4
4
  import { observationImages, renderObservationResult, selectObservationImages } from '../tools/code-mode/observations.js';
5
5
  import { INLINE_MEDIA_POLICY as policy, assertInlineMediaBudget, inlineMediaBytes, inlineMediaHistory, inlineMediaLimit, inlineMediaSource } from '../session/inline-media.js';
6
- function releasedImageNotice(message, indices) {
6
+ function releasedImageNotice(message, indices, unavailable) {
7
7
  return indices
8
8
  .map((index) => {
9
+ if (unavailable?.has(index))
10
+ return 'Image unavailable; omitted from context.';
9
11
  const resourceId = message.images?.[index]?.resourceId;
10
12
  const hint = resourceId
11
13
  ? ` Reopen with view_image(${JSON.stringify({ resourceId })}) if needed.`
@@ -14,7 +16,7 @@ function releasedImageNotice(message, indices) {
14
16
  })
15
17
  .join('\n');
16
18
  }
17
- function projectRetainedImages(message, images, removed) {
19
+ function projectRetainedImages(message, images, removed, unavailable) {
18
20
  if (!removed.size)
19
21
  return { ...message, images };
20
22
  if (message.observations) {
@@ -22,7 +24,7 @@ function projectRetainedImages(message, images, removed) {
22
24
  const removedIndices = observationImages(observation).filter((index) => removed.has(index));
23
25
  if (!removedIndices.length)
24
26
  return observation;
25
- const notice = releasedImageNotice(message, removedIndices);
27
+ const notice = releasedImageNotice(message, removedIndices, unavailable);
26
28
  return observation.type === 'tool_result'
27
29
  ? {
28
30
  ...observation,
@@ -37,7 +39,7 @@ function projectRetainedImages(message, images, removed) {
37
39
  content: renderObservationResult(selected.observations, message.content)
38
40
  };
39
41
  }
40
- const notice = releasedImageNotice(message, [...removed]);
42
+ const notice = releasedImageNotice(message, [...removed], unavailable);
41
43
  return {
42
44
  ...message,
43
45
  images: images.filter(Boolean),
@@ -54,6 +56,8 @@ export class ResourceInputProjection {
54
56
  #entries = [];
55
57
  #sources = new Map();
56
58
  #candidates = new WeakMap();
59
+ #resourceReads = new Map();
60
+ #cachedBytes = 0;
57
61
  #considered = -1;
58
62
  #rollout = 0;
59
63
  #evicted = -1;
@@ -98,12 +102,15 @@ export class ResourceInputProjection {
98
102
  for (const message of messages)
99
103
  for (const [index, image] of (message.images ?? []).entries()) {
100
104
  const entry = this.#entries[this.#entry(inlineMediaSource(message), index)];
105
+ if (entry.image && !isDeepStrictEqual(entry.image, image))
106
+ delete entry.unavailable;
101
107
  entry.image = image;
102
108
  entry.active = true;
103
109
  }
104
110
  }
105
111
  /** Called only at an explicit context replacement, never as a history diff detector. */
106
112
  replace(messages) {
113
+ this.releaseBytes();
107
114
  for (const entry of this.#entries) {
108
115
  entry.active = false;
109
116
  delete entry.image;
@@ -133,16 +140,23 @@ export class ResourceInputProjection {
133
140
  if (!images?.length)
134
141
  return message;
135
142
  const removed = new Set();
143
+ const unavailable = new Set();
136
144
  const retained = [];
137
145
  images.forEach((image, index) => {
138
- if (!this.useResourceUrl &&
146
+ const position = positions?.[index];
147
+ const entry = position === undefined ? undefined : this.#entries[position];
148
+ if (entry?.unavailable && isDeepStrictEqual(entry.image, image)) {
149
+ removed.add(index);
150
+ unavailable.add(index);
151
+ }
152
+ else if (!this.useResourceUrl &&
139
153
  positions?.[index] !== undefined &&
140
154
  positions[index] <= evicted)
141
155
  removed.add(index);
142
156
  else
143
157
  retained[index] = image;
144
158
  });
145
- return projectRetainedImages({ ...message, images }, retained, removed);
159
+ return projectRetainedImages({ ...message, images }, retained, removed, unavailable);
146
160
  });
147
161
  }
148
162
  checkpoint(messages) {
@@ -167,50 +181,44 @@ export class ResourceInputProjection {
167
181
  this.observe(canonicalMessages);
168
182
  const through = this.#entries.length - 1;
169
183
  const cached = new Map();
170
- const project = (image) => {
171
- let value = cached.get(image);
184
+ const project = (image, entry) => {
185
+ const key = entry && isDeepStrictEqual(entry.image, image) ? entry : image;
186
+ let value = cached.get(key);
172
187
  if (!value)
173
- cached.set(image, (value = this.#project(image)));
188
+ cached.set(key, (value = this.#project(image, entry)));
174
189
  return value;
175
190
  };
176
- const sized = async (image) => image.dataBase64 !== undefined || image.byteSize !== undefined ? image : project(image);
177
191
  const top = (request.images ?? []).filter((image) => !mirroredImages.some((mirror) => isDeepStrictEqual(image, mirror)));
178
192
  const evicted = this.#evicted;
179
- if (!this.useResourceUrl) {
180
- let total = 0;
181
- for (const message of request.messages)
182
- for (const [index, image] of (message.images ?? []).entries()) {
183
- const position = this.#sources.get(inlineMediaSource(message))?.[index];
184
- const original = position === undefined ? undefined : this.#entries[position]?.image;
185
- if (position !== undefined && position <= evicted && isDeepStrictEqual(original, image))
186
- continue;
187
- total += inlineMediaBytes(await sized(image));
188
- }
189
- for (const image of top)
190
- total += inlineMediaBytes(await sized(image));
191
- if (total > policy.requestInlineMediaHardLimit)
192
- inlineMediaLimit();
193
- }
194
193
  const messages = await Promise.all(request.messages.map(async (message) => {
195
194
  if (!message.images?.length)
196
195
  return message;
197
196
  const removed = new Set();
197
+ const unavailable = new Set();
198
198
  const images = [];
199
199
  await Promise.all(message.images.map(async (image, index) => {
200
200
  const position = this.#sources.get(inlineMediaSource(message))?.[index];
201
- const original = position === undefined ? undefined : this.#entries[position]?.image;
201
+ const entry = position === undefined ? undefined : this.#entries[position];
202
+ const original = entry?.image;
202
203
  if (!this.useResourceUrl &&
203
204
  position !== undefined &&
204
205
  position <= evicted &&
205
206
  isDeepStrictEqual(original, image))
206
207
  removed.add(index);
207
- else
208
- images[index] = await project(image);
208
+ else {
209
+ const projected = await project(image, entry);
210
+ if (projected)
211
+ images[index] = projected;
212
+ else {
213
+ removed.add(index);
214
+ unavailable.add(index);
215
+ }
216
+ }
209
217
  }));
210
- return projectRetainedImages(message, images, removed);
218
+ return projectRetainedImages(message, images, removed, unavailable);
211
219
  }));
212
220
  const { images: _topImages, ...retained } = request;
213
- const images = await Promise.all(top.map(project));
221
+ const images = (await Promise.all(top.map((image) => project(image)))).filter((image) => image !== undefined);
214
222
  const result = { ...retained, messages, ...(images.length ? { images } : {}) };
215
223
  if (!this.useResourceUrl)
216
224
  assertInlineMediaBudget(result);
@@ -241,16 +249,25 @@ export class ResourceInputProjection {
241
249
  async gcCandidate(request, mirroredImages = []) {
242
250
  if (this.useResourceUrl)
243
251
  return undefined;
244
- const size = async (image) => inlineMediaBytes(image.byteSize !== undefined || image.dataBase64 !== undefined
245
- ? image
246
- : await this.#project(image));
252
+ const size = async (image, entry) => {
253
+ if (entry?.unavailable && isDeepStrictEqual(entry.image, image))
254
+ return 0;
255
+ const sized = image.byteSize !== undefined || image.dataBase64 !== undefined
256
+ ? image
257
+ : await this.#project(image, entry);
258
+ return sized ? inlineMediaBytes(sized) : 0;
259
+ };
260
+ const actualSize = async (image, entry) => {
261
+ const projected = await this.#project(image, entry);
262
+ return projected ? inlineMediaBytes(projected) : 0;
263
+ };
247
264
  const history = await Promise.all(this.#entries.map(async (entry, index) => ({
248
265
  index,
249
266
  bytes: entry.active && entry.image && index > this.#evicted && index <= this.#considered
250
- ? await size(entry.image)
267
+ ? await size(entry.image, entry)
251
268
  : 0
252
269
  })));
253
- const beforeBytes = history.reduce((sum, entry) => sum + entry.bytes, 0);
270
+ let beforeBytes = history.reduce((sum, entry) => sum + entry.bytes, 0);
254
271
  let freshBytes = 0;
255
272
  let requestBytes = 0;
256
273
  const requestHistory = new Map();
@@ -262,20 +279,23 @@ export class ResourceInputProjection {
262
279
  const original = entry && isDeepStrictEqual(entry.image, image);
263
280
  if (original && position <= this.#evicted)
264
281
  continue;
265
- const bytes = await size(image);
282
+ const bytes = await actualSize(image, entry);
266
283
  requestBytes += bytes;
267
- if (original && position <= this.#considered)
284
+ if (original && position <= this.#considered) {
268
285
  requestHistory.set(position, (requestHistory.get(position) ?? 0) + bytes);
286
+ history[position].bytes = bytes;
287
+ }
269
288
  else
270
289
  freshBytes += bytes;
271
290
  }
272
291
  for (const image of request.images ?? []) {
273
292
  if (mirroredImages.some((mirror) => isDeepStrictEqual(image, mirror)))
274
293
  continue;
275
- const bytes = await size(image);
294
+ const bytes = await actualSize(image);
276
295
  freshBytes += bytes;
277
296
  requestBytes += bytes;
278
297
  }
298
+ beforeBytes = history.reduce((sum, item) => sum + item.bytes, 0);
279
299
  if (freshBytes > policy.requestInlineMediaHardLimit)
280
300
  inlineMediaLimit();
281
301
  }
@@ -319,23 +339,64 @@ export class ResourceInputProjection {
319
339
  };
320
340
  }
321
341
  adoptGc(state) {
322
- if (state)
342
+ if (state) {
323
343
  this.#evicted = this.#position(state.evictedThrough);
344
+ this.releaseBytes();
345
+ }
346
+ }
347
+ releaseBytes() {
348
+ this.#resourceReads.clear();
349
+ this.#cachedBytes = 0;
350
+ }
351
+ #readBytes(resourceId) {
352
+ const cached = this.#resourceReads.get(resourceId);
353
+ if (cached)
354
+ return cached;
355
+ const pending = this.store.readResource(this.sessionId, resourceId).then((resource) => {
356
+ if (this.#resourceReads.get(resourceId) === pending) {
357
+ const bytes = resource?.dataBase64 ? inlineMediaBytes(resource) : 0;
358
+ if (!bytes || this.#cachedBytes + bytes > policy.requestInlineMediaHardLimit)
359
+ this.#resourceReads.delete(resourceId);
360
+ else
361
+ this.#cachedBytes += bytes;
362
+ }
363
+ return resource;
364
+ }, (error) => {
365
+ if (this.#resourceReads.get(resourceId) === pending)
366
+ this.#resourceReads.delete(resourceId);
367
+ throw error;
368
+ });
369
+ this.#resourceReads.set(resourceId, pending);
370
+ return pending;
324
371
  }
325
- async #project(image) {
372
+ async #project(image, entry) {
373
+ const original = entry && isDeepStrictEqual(entry.image, image);
374
+ if (original && entry.unavailable)
375
+ return undefined;
326
376
  if (this.useResourceUrl) {
327
377
  if (!isModelImageUrl(image.url))
328
378
  throw new MarAgentError('MAR_AGENT_RESOURCE_URL_UNAVAILABLE', 'Resource URL is unavailable.');
329
379
  const { dataBase64: _bytes, ...reference } = image;
330
380
  return reference;
331
381
  }
332
- const bytes = image.dataBase64 !== undefined
333
- ? image
334
- : image.resourceId === undefined
335
- ? undefined
336
- : await this.store.readResource(this.sessionId, image.resourceId);
337
- if (!bytes?.dataBase64)
338
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Resource bytes are unavailable.');
382
+ let bytes;
383
+ try {
384
+ bytes =
385
+ image.dataBase64 !== undefined
386
+ ? image
387
+ : image.resourceId === undefined
388
+ ? undefined
389
+ : await this.#readBytes(image.resourceId);
390
+ }
391
+ catch (error) {
392
+ if (!(error instanceof MarAgentError) || error.code !== 'MAR_AGENT_IMAGE_UNAVAILABLE')
393
+ throw error;
394
+ }
395
+ if (!bytes?.dataBase64) {
396
+ if (original)
397
+ entry.unavailable = true;
398
+ return undefined;
399
+ }
339
400
  const { url: _url, urlExpiresAt: _expires, ...reference } = image;
340
401
  return { ...reference, dataBase64: bytes.dataBase64 };
341
402
  }
@@ -389,6 +389,7 @@ export function createSessionOpener(services) {
389
389
  let finalAnswer = '';
390
390
  let executionLoop;
391
391
  let executionToolSources;
392
+ let releaseImageBytes;
392
393
  const contextGc = new ContextGcRuntime();
393
394
  let selectedAdapter;
394
395
  const modelResources = createExecutionModelResources(activeExecutionAdapters);
@@ -816,6 +817,7 @@ export function createSessionOpener(services) {
816
817
  }));
817
818
  }
818
819
  });
820
+ releaseImageBytes = () => resourceInput.releaseBytes();
819
821
  const selected = modelResources.openTurn(selectedAdapter, services.hasCustomAdapters && selectedAdapter.createTurnSession === undefined);
820
822
  let lastServerUsage = latestModelUsage(contextRecords, selectedModel.id);
821
823
  let smartContextTokens = latestSmartContextTokens(contextRecords, selectedModel.id, selectedModel.contextWindowTokens);
@@ -1981,6 +1983,7 @@ export function createSessionOpener(services) {
1981
1983
  };
1982
1984
  }
1983
1985
  finally {
1986
+ releaseImageBytes?.();
1984
1987
  // Revoke this Execution's request and input bindings, not Session warm resources.
1985
1988
  delete executionContext.requestMessages;
1986
1989
  compactRequests.close(new MarAgentError('MAR_AGENT_CANCELLED', 'Execution ended.'));
@@ -1,6 +1,4 @@
1
- import { createHash } from 'node:crypto';
2
- import { constants } from 'node:fs';
3
- import { copyFile, lstat, mkdir, open } from 'node:fs/promises';
1
+ import { copyFile, mkdir, readFile } from 'node:fs/promises';
4
2
  import { basename, join } from 'node:path';
5
3
  import { MarAgentError } from '../../error.js';
6
4
  import { isModelImageUrl } from '../../model/image.js';
@@ -93,57 +91,14 @@ export function validInputImageFileName(fileName) {
93
91
  return (basename(fileName) === fileName && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}\.bin$/u.test(fileName));
94
92
  }
95
93
  export async function readInputImageBytes(directory, reference) {
96
- const path = join(directory, reference.fileName);
97
- for (let attempt = 0; attempt < 2; attempt++) {
98
- let handle;
99
- try {
100
- const before = await lstat(path);
101
- if (!before.isFile() || before.size !== reference.byteSize)
102
- throw corruptInputImageReference();
103
- const flags = process.platform === 'win32'
104
- ? constants.O_RDONLY
105
- : constants.O_RDONLY | constants.O_NOFOLLOW;
106
- handle = await open(path, flags);
107
- const opened = await handle.stat();
108
- if (!opened.isFile() ||
109
- opened.size !== reference.byteSize ||
110
- opened.dev !== before.dev ||
111
- opened.ino !== before.ino)
112
- continue;
113
- const data = await readExactBytes(handle, reference.byteSize);
114
- const after = await handle.stat();
115
- if (after.size !== opened.size ||
116
- after.mtimeMs !== opened.mtimeMs ||
117
- after.ctimeMs !== opened.ctimeMs)
118
- continue;
119
- if (createHash('sha256').update(data).digest('hex') !== reference.sha256)
120
- throw corruptInputImageReference();
121
- return data;
122
- }
123
- catch (cause) {
124
- if (cause instanceof MarAgentError)
125
- throw cause;
126
- throw corruptInputImageReference(cause);
127
- }
128
- finally {
129
- await handle?.close();
130
- }
94
+ try {
95
+ return await readFile(join(directory, reference.fileName));
131
96
  }
132
- throw corruptInputImageReference();
133
- }
134
- async function readExactBytes(handle, byteSize) {
135
- const data = Buffer.alloc(byteSize);
136
- let offset = 0;
137
- while (offset < byteSize) {
138
- const { bytesRead } = await handle.read(data, offset, byteSize - offset, offset);
139
- if (bytesRead === 0)
140
- throw corruptInputImageReference();
141
- offset += bytesRead;
97
+ catch (cause) {
98
+ throw new MarAgentError('MAR_AGENT_IMAGE_UNAVAILABLE', 'Session input image is unavailable.', {
99
+ cause
100
+ });
142
101
  }
143
- const extra = Buffer.allocUnsafe(1);
144
- if ((await handle.read(extra, 0, 1, byteSize)).bytesRead !== 0)
145
- throw corruptInputImageReference();
146
- return data;
147
102
  }
148
103
  function corruptInputImageReference(cause) {
149
104
  return new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session input image is corrupt.', {
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { inlineMediaHistory } from '../inline-media.js';
3
- import { lstat, mkdir, open, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { lstat, mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
4
4
  import { basename, join } from 'node:path';
5
5
  import { isDeepStrictEqual } from 'node:util';
6
6
  import { MarAgentError } from '../../error.js';
@@ -394,27 +394,15 @@ export class JsonlSessionStore {
394
394
  async readContextualWorldState(sessionId) {
395
395
  const path = join(this.#sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
396
396
  try {
397
- const pathInfo = await lstat(path);
398
- if (!pathInfo.isFile())
397
+ const envelope = JSON.parse(await readFile(path, 'utf8'));
398
+ const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
399
+ if (envelope.version !== 1 ||
400
+ !Number.isSafeInteger(envelope.historySequence) ||
401
+ envelope.historySequence < 1 ||
402
+ meta === undefined ||
403
+ envelope.historySequence >= meta.nextSequence)
399
404
  return undefined;
400
- const file = await open(path, 'r');
401
- try {
402
- const info = await file.stat();
403
- if (!info.isFile() || info.size > MAX_CONTEXTUAL_WORLD_STATE_BYTES)
404
- return undefined;
405
- const envelope = JSON.parse(await file.readFile('utf8'));
406
- const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
407
- if (envelope.version !== 1 ||
408
- !Number.isSafeInteger(envelope.historySequence) ||
409
- envelope.historySequence < 1 ||
410
- meta === undefined ||
411
- envelope.historySequence >= meta.nextSequence)
412
- return undefined;
413
- return envelope.state;
414
- }
415
- finally {
416
- await file.close();
417
- }
405
+ return envelope.state;
418
406
  }
419
407
  catch {
420
408
  return undefined;
@@ -490,16 +478,21 @@ export class JsonlSessionStore {
490
478
  if (!/^[a-f0-9]{64}$/u.test(resourceId))
491
479
  return undefined;
492
480
  return this.#serializeRecordOperation(sessionId, async () => {
481
+ let pending = false;
493
482
  try {
494
483
  await lstat(join(directory, `.${resourceId}.pending`));
495
- if (this.#shared.writerLeases.get(sessionId)?.appendFailure !== undefined ||
496
- !resourceFiles(await this.#readRecords(sessionId)).has(`${resourceId}.bin`))
497
- return undefined;
484
+ pending = true;
498
485
  }
499
- catch (error) {
500
- if (error.code !== 'ENOENT')
501
- throw error;
486
+ catch (cause) {
487
+ if (cause.code !== 'ENOENT')
488
+ throw new MarAgentError('MAR_AGENT_IMAGE_UNAVAILABLE', 'Session image resource is unavailable.', {
489
+ cause
490
+ });
502
491
  }
492
+ if (pending &&
493
+ (this.#shared.writerLeases.get(sessionId)?.appendFailure !== undefined ||
494
+ !resourceFiles(await this.#readRecords(sessionId)).has(`${resourceId}.bin`)))
495
+ return undefined;
503
496
  return readResource(directory, resourceId);
504
497
  });
505
498
  }
@@ -12,44 +12,39 @@ function resourceIdentity(mimeType, sha256) {
12
12
  export async function resourceMetadata(directory, resourceId) {
13
13
  if (!idPattern.test(resourceId))
14
14
  return undefined;
15
- let text;
16
15
  try {
17
- text = await readFile(join(directory, `${resourceId}.json`), 'utf8');
16
+ const value = JSON.parse(await readFile(join(directory, `${resourceId}.json`), 'utf8'));
17
+ const resource = value;
18
+ if (!value ||
19
+ typeof value !== 'object' ||
20
+ Array.isArray(value) ||
21
+ typeof resource.resourceId !== 'string' ||
22
+ typeof resource.mimeType !== 'string' ||
23
+ typeof resource.byteSize !== 'number' ||
24
+ typeof resource.sha256 !== 'string' ||
25
+ (resource.url !== undefined &&
26
+ (typeof resource.url !== 'string' ||
27
+ !isModelImageUrl(resource.url) ||
28
+ !Number.isSafeInteger(resource.urlExpiresAt) ||
29
+ resource.urlExpiresAt <= 0)) ||
30
+ (resource.url === undefined && resource.urlExpiresAt !== undefined))
31
+ throw new Error('Invalid Session image resource metadata.');
32
+ return value;
18
33
  }
19
- catch (error) {
20
- if (error.code === 'ENOENT')
34
+ catch (cause) {
35
+ if (cause.code === 'ENOENT')
21
36
  return undefined;
22
- throw error;
23
- }
24
- let value;
25
- try {
26
- value = JSON.parse(text);
27
- }
28
- catch {
29
- // Invalid JSON is reported below without exposing metadata contents.
37
+ throw new MarAgentError('MAR_AGENT_IMAGE_UNAVAILABLE', 'Session image resource is unavailable.', {
38
+ cause
39
+ });
30
40
  }
31
- if (!value ||
32
- value.resourceId !== resourceId ||
33
- !mimeTypes.has(value.mimeType) ||
34
- !Number.isSafeInteger(value.byteSize) ||
35
- value.byteSize <= 0 ||
36
- value.byteSize > 20 * 1024 * 1024 ||
37
- !idPattern.test(value.sha256) ||
38
- resourceIdentity(value.mimeType, value.sha256) !== resourceId ||
39
- (value.url !== undefined &&
40
- (!isModelImageUrl(value.url) ||
41
- !Number.isSafeInteger(value.urlExpiresAt) ||
42
- value.urlExpiresAt <= 0)) ||
43
- (value.url === undefined && value.urlExpiresAt !== undefined))
44
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session image resource is corrupt.');
45
- return value;
46
41
  }
47
42
  export async function persistResource(directory, image) {
48
43
  if (image.resourceId !== undefined && image.dataBase64 === undefined) {
49
44
  const existing = await resourceMetadata(directory, image.resourceId);
50
45
  if (existing !== undefined && existing.mimeType === image.mimeType)
51
46
  return existing;
52
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session image resource is missing.');
47
+ throw new MarAgentError('MAR_AGENT_IMAGE_UNAVAILABLE', 'Session image resource is missing.');
53
48
  }
54
49
  const data = Buffer.from(image.dataBase64 ?? '', 'base64');
55
50
  if (data.toString('base64') !== image.dataBase64)
@@ -60,10 +55,16 @@ export async function persistResource(directory, image) {
60
55
  const resourceId = resourceIdentity(image.mimeType, sha256);
61
56
  if (image.resourceId !== undefined && image.resourceId !== resourceId)
62
57
  throw new MarAgentError('MAR_AGENT_SESSION_INPUT_INVALID', 'Image resource identity does not match its bytes.');
63
- const existing = await resourceMetadata(directory, resourceId);
64
- if (existing !== undefined)
65
- return existing;
58
+ let existing;
59
+ try {
60
+ existing = await resourceMetadata(directory, resourceId);
61
+ }
62
+ catch (error) {
63
+ if (!(error instanceof MarAgentError) || error.code !== 'MAR_AGENT_IMAGE_UNAVAILABLE')
64
+ throw error;
65
+ }
66
66
  const resource = {
67
+ ...existing,
67
68
  resourceInputVersion: 1,
68
69
  resourceId,
69
70
  mimeType: image.mimeType,
@@ -79,7 +80,7 @@ export async function persistResource(directory, image) {
79
80
  export async function setResourceUrl(directory, resourceId, published) {
80
81
  const resource = await resourceMetadata(directory, resourceId);
81
82
  if (!resource)
82
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session image resource is missing.');
83
+ throw new MarAgentError('MAR_AGENT_IMAGE_UNAVAILABLE', 'Session image resource is missing.');
83
84
  if (resource.url !== undefined)
84
85
  return resource;
85
86
  if (!isModelImageUrl(published.url) ||
@@ -136,7 +137,7 @@ export async function copyResources(source, target, records) {
136
137
  const resourceId = file.slice(0, -4);
137
138
  const resource = await resourceMetadata(join(source, 'resources'), resourceId);
138
139
  if (resource === undefined)
139
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session image resource is missing.');
140
+ throw new MarAgentError('MAR_AGENT_IMAGE_UNAVAILABLE', 'Session image resource is missing.');
140
141
  const bytes = await readInputImageBytes(join(source, 'resources'), {
141
142
  ...resource,
142
143
  name: 'image',
@@ -1,6 +1,5 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { constants } from 'node:fs';
3
- import { lstat, mkdir, open, readdir, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
4
3
  import { join } from 'node:path';
5
4
  import { MarAgentError } from '../error.js';
6
5
  import { parseToolResultExtensions } from '../tools/result-extensions.js';
@@ -79,12 +78,8 @@ async function bindToolResultResource(directory, record, resource, readRecord) {
79
78
  catch (error) {
80
79
  if (error.code !== 'EEXIST')
81
80
  throw error;
82
- const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
83
81
  try {
84
- const info = await handle.stat();
85
- if (!info.isFile() || info.size > 1024)
86
- throw resourceError('CORRUPT', 'Invalid tool resource locator.');
87
- const locator = JSON.parse(await handle.readFile('utf8'));
82
+ const locator = JSON.parse(await readFile(path, 'utf8'));
88
83
  const existing = locator.sequence === record.sequence && locator.recordId === record.recordId
89
84
  ? record
90
85
  : Number.isSafeInteger(locator.sequence)
@@ -98,9 +93,6 @@ async function bindToolResultResource(directory, record, resource, readRecord) {
98
93
  catch (cause) {
99
94
  throw normalizedResourceError(cause);
100
95
  }
101
- finally {
102
- await handle.close();
103
- }
104
96
  }
105
97
  }
106
98
  export function toolResultResourceFiles(records) {
@@ -184,16 +176,7 @@ export async function readToolResultResource(directory, sessionId, input, readRe
184
176
  let locator;
185
177
  try {
186
178
  const file = join(directory, 'tool_results', `${input.resourceRef}.json`);
187
- const handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
188
- try {
189
- const info = await handle.stat();
190
- if (!info.isFile() || info.size > 1024)
191
- throw resourceError('CORRUPT', 'Invalid tool resource locator.');
192
- locator = JSON.parse(await handle.readFile('utf8'));
193
- }
194
- finally {
195
- await handle.close();
196
- }
179
+ locator = JSON.parse(await readFile(file, 'utf8'));
197
180
  }
198
181
  catch (error) {
199
182
  throw normalizedResourceError(error);
@@ -208,6 +191,7 @@ export async function readToolResultResource(directory, sessionId, input, readRe
208
191
  : undefined;
209
192
  if (!resource)
210
193
  throw resourceError('NOT_FOUND', 'Tool resource is not part of this session history.');
194
+ const bytes = await readResourceBytes(directory, resource);
211
195
  let offset = 0;
212
196
  if (input.cursor !== undefined) {
213
197
  try {
@@ -219,7 +203,7 @@ export async function readToolResultResource(directory, sessionId, input, readRe
219
203
  cursor.sha256 !== resource.sha256 ||
220
204
  !Number.isSafeInteger(cursor.offset) ||
221
205
  cursor.offset < 0 ||
222
- cursor.offset >= resource.byteSize)
206
+ cursor.offset >= bytes.length)
223
207
  throw new Error('cursor');
224
208
  offset = cursor.offset;
225
209
  }
@@ -227,7 +211,6 @@ export async function readToolResultResource(directory, sessionId, input, readRe
227
211
  throw resourceError('INPUT_INVALID', 'Invalid tool resource cursor.');
228
212
  }
229
213
  }
230
- const bytes = await readResourceBytes(directory, resource);
231
214
  if (offset > 0 && (bytes[offset] & 0xc0) === 0x80)
232
215
  throw resourceError('INPUT_INVALID', 'Invalid tool resource cursor boundary.');
233
216
  let end = Math.min(bytes.length, offset + (input.maxBytes ?? 256 * 1024));
@@ -257,19 +240,7 @@ export async function readToolResultResource(directory, sessionId, input, readRe
257
240
  }
258
241
  async function readResourceBytes(directory, resource) {
259
242
  try {
260
- const handle = await open(join(directory, 'tool_results', `${resource.resourceRef}.diff`), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
261
- try {
262
- const stat = await handle.stat();
263
- if (!stat.isFile() || stat.size !== resource.byteSize || stat.size > MAX_BYTES)
264
- throw resourceError('CORRUPT', 'Tool resource size does not match its history.');
265
- const bytes = await handle.readFile();
266
- if (createHash('sha256').update(bytes).digest('hex') !== resource.sha256)
267
- throw resourceError('CORRUPT', 'Tool resource checksum does not match its history.');
268
- return bytes;
269
- }
270
- finally {
271
- await handle.close();
272
- }
243
+ return await readFile(join(directory, 'tool_results', `${resource.resourceRef}.diff`));
273
244
  }
274
245
  catch (error) {
275
246
  throw normalizedResourceError(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.11.24",
3
+ "version": "0.11.25",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",