@myagentroam/agent 0.9.157 → 0.9.158

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.
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.9.157",
3
+ "version": "0.9.158",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",