@myagentroam/node 0.9.134 → 0.9.136

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.
Files changed (29) hide show
  1. package/dist/capabilities.js +1 -0
  2. package/dist/codex-app-server.js +14 -0
  3. package/dist/connector.js +4 -2
  4. package/dist/node-execution-environment.js +8 -0
  5. package/dist/runner/abstract-runner.js +35 -4
  6. package/dist/runner/claude/managed-run-controller.js +8 -1
  7. package/dist/runner/codex/conversation-parser.js +194 -13
  8. package/dist/runner/codex/managed-run-controller.js +2 -1
  9. package/dist/runner/codex-runner.js +113 -10
  10. package/dist/runner/mar-agent/computer-use.js +1 -0
  11. package/dist/runner/mar-agent/conversation-normalizer.js +8 -6
  12. package/dist/runner/mar-agent/hosted-conversation-runtime.js +3 -2
  13. package/dist/runner/mar-agent/managed-run-controller.js +46 -8
  14. package/dist/runner/mar-agent/mcp-client-pool.js +253 -29
  15. package/dist/runner/mar-agent/runtime-model-factory.js +7 -2
  16. package/dist/runner/mar-agent-runner.js +3 -0
  17. package/dist/runner/opencode/managed-run-controller.js +2 -1
  18. package/dist/runtime-state.js +2 -2
  19. package/dist/service/continuous-run-coordinator.js +3 -1
  20. package/dist/service/conversation-history-service.js +29 -40
  21. package/dist/service/external-session-resume-service.js +2 -0
  22. package/dist/service/hosted-conversation-node-service.js +1 -1
  23. package/dist/service/native-session-projection-service.js +2 -0
  24. package/dist/service/native-session-watch-service.js +13 -4
  25. package/dist/service/run-event-service.js +13 -0
  26. package/dist/service/session-catalog-service.js +3 -4
  27. package/dist/service/session-message-service.js +4 -2
  28. package/dist/util/node-operation-parsers.js +1 -3
  29. package/package.json +6 -5
@@ -1,7 +1,9 @@
1
- import { createHash } from 'node:crypto';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
3
  import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
4
4
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
5
+ import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
6
+ import { assertMcpToolDefinition, assertMcpToolCatalog, MarAgentError } from '@myagentroam/agent';
5
7
  import { ExecutorStdioClientTransport } from './executor-stdio-client-transport.js';
6
8
  export class MarAgentMcpClientCache {
7
9
  #entries = new Map();
@@ -21,16 +23,16 @@ export class MarAgentMcpClientCache {
21
23
  const pool = await MarAgentMcpClientPool.connect(servers, cwd, options);
22
24
  return singleUseLease(pool);
23
25
  }
24
- const key = mcpConfigurationFingerprint(servers, cwd);
26
+ const key = mcpConfigurationFingerprint(servers, cwd, options.authorizationScope);
25
27
  let entry = this.#entries.get(key);
26
28
  if (entry === undefined) {
27
29
  await this.#evictIdleEntry();
28
30
  if (this.#entries.size >= this.#maxEntries) {
29
- const pool = await MarAgentMcpClientPool.connect(servers, cwd);
31
+ const pool = await MarAgentMcpClientPool.connect(servers, cwd, options);
30
32
  return singleUseLease(pool);
31
33
  }
32
34
  entry = {
33
- promise: MarAgentMcpClientPool.connect(servers, cwd),
35
+ promise: MarAgentMcpClientPool.connect(servers, cwd, options),
34
36
  references: 0,
35
37
  lastReleasedAt: 0
36
38
  };
@@ -103,37 +105,87 @@ export class MarAgentMcpClientCache {
103
105
  await (await entry.promise).close().catch(() => undefined);
104
106
  }
105
107
  }
108
+ export const MCP_CATALOG_LIMITS = Object.freeze({
109
+ httpConcurrency: 4,
110
+ retryMs: 30_000,
111
+ timeoutMs: 30_000,
112
+ maximumPages: 256,
113
+ maximumTools: 10_000,
114
+ maximumBytes: 16 * 1024 * 1024
115
+ });
106
116
  export class MarAgentMcpClientPool {
117
+ #unknownPrincipal = randomUUID();
107
118
  #clients = [];
119
+ #servers = new Map();
108
120
  #tools = new Map();
121
+ #snapshot = { revision: '', servers: [], tools: [] };
122
+ #controller = new AbortController();
123
+ #closed = false;
124
+ #ready = false;
125
+ #initialCatalogAvailable = true;
126
+ #httpRefreshActive = 0;
127
+ #httpWaiters = [];
109
128
  static async connect(servers, cwd, options = {}) {
110
129
  const pool = new MarAgentMcpClientPool();
111
- try {
112
- for (const server of servers)
130
+ const http = servers.filter((server) => server.transport === 'HTTP');
131
+ let next = 0;
132
+ const jobs = Array.from({ length: Math.min(MCP_CATALOG_LIMITS.httpConcurrency, http.length) }, async () => {
133
+ while (next < http.length)
134
+ await pool.#connect(http[next++], cwd, options);
135
+ });
136
+ jobs.push((async () => {
137
+ for (const server of servers.filter((server) => server.transport === 'STDIO'))
113
138
  await pool.#connect(server, cwd, options);
139
+ })());
140
+ try {
141
+ await Promise.all(jobs);
142
+ pool.#publish();
143
+ pool.#ready = true;
144
+ for (const state of pool.#servers.values())
145
+ if (state.dirty)
146
+ void pool.#refresh(state).catch(() => undefined);
114
147
  return pool;
115
148
  }
116
149
  catch (error) {
150
+ pool.#controller.abort();
151
+ await Promise.allSettled(jobs);
117
152
  await pool.close();
118
153
  throw error;
119
154
  }
120
155
  }
121
156
  definitions() {
122
- return [...this.#tools].map(([name, binding]) => ({
123
- name,
124
- description: binding.description,
125
- inputSchema: binding.inputSchema
126
- }));
157
+ return this.#snapshot.tools.map((tool) => tool.definition);
158
+ }
159
+ catalog() {
160
+ return this.#snapshot;
127
161
  }
128
- async call(name, arguments_) {
162
+ async refreshForExecution() {
163
+ if (this.#initialCatalogAvailable) {
164
+ this.#initialCatalogAvailable = false;
165
+ await Promise.all([...this.#servers.values()].flatMap((state) => state.refreshing ? [state.refreshing.catch(() => undefined)] : []));
166
+ return;
167
+ }
168
+ await Promise.all([...this.#servers.values()].map((state) => this.#refresh(state).catch(() => undefined)));
169
+ }
170
+ async call(name, arguments_, signal, expectedTool) {
129
171
  const binding = this.#tools.get(name);
130
- if (!binding)
131
- throw new Error('MAR_AGENT_MCP_TOOL_NOT_FOUND');
172
+ if (!binding || this.#closed)
173
+ throw new MarAgentError('MAR_AGENT_MCP_TOOL_NOT_FOUND', 'MCP tool is no longer available.');
174
+ if (expectedTool &&
175
+ (binding.metadata.sourceIdentity !== expectedTool.sourceIdentity ||
176
+ binding.metadata.definitionRevision !== expectedTool.definitionRevision))
177
+ throw new MarAgentError('MAR_AGENT_MCP_TOOL_DEFINITION_CHANGED', 'MCP tool binding or definition changed.');
178
+ signal?.throwIfAborted();
132
179
  const result = await binding.client.callTool({
133
180
  name: binding.nativeName,
134
181
  arguments: arguments_ !== null && typeof arguments_ === 'object' && !Array.isArray(arguments_)
135
182
  ? arguments_
136
183
  : {}
184
+ }, undefined, {
185
+ timeout: MCP_CATALOG_LIMITS.timeoutMs,
186
+ signal: signal
187
+ ? AbortSignal.any([signal, this.#controller.signal])
188
+ : this.#controller.signal
137
189
  });
138
190
  const items = Array.isArray(result.content) ? result.content : [];
139
191
  const content = items
@@ -149,12 +201,156 @@ export class MarAgentMcpClientPool {
149
201
  return { content, data: result.structuredContent ?? items };
150
202
  }
151
203
  async close() {
204
+ if (this.#closed)
205
+ return;
206
+ this.#closed = true;
207
+ this.#controller.abort();
208
+ await Promise.allSettled([...this.#servers.values()].flatMap((state) => (state.refreshing ? [state.refreshing] : [])));
152
209
  await Promise.all(this.#clients.map((client) => client.close().catch(() => undefined)));
153
210
  this.#clients.length = 0;
211
+ this.#servers.clear();
154
212
  this.#tools.clear();
213
+ this.#snapshot = { revision: '', servers: [], tools: [] };
214
+ }
215
+ #publish(replacement) {
216
+ const combined = new Map();
217
+ for (const state of this.#servers.values())
218
+ for (const [name, binding] of replacement?.state === state
219
+ ? replacement.tools
220
+ : state.tools) {
221
+ if (combined.has(name))
222
+ throw new Error('MAR_AGENT_MCP_TOOL_NAME_CONFLICT');
223
+ combined.set(name, binding);
224
+ }
225
+ const tools = [...combined]
226
+ .sort(([a], [b]) => a.localeCompare(b))
227
+ .map(([, binding]) => binding.metadata);
228
+ const servers = [...this.#servers.values()]
229
+ .map((state) => state.metadata)
230
+ .sort((a, b) => a.id.localeCompare(b.id));
231
+ const revision = fingerprint({ servers, tools });
232
+ assertMcpToolCatalog({ revision, servers, tools });
233
+ if (replacement)
234
+ replacement.state.tools = replacement.tools;
235
+ this.#tools = combined;
236
+ if (revision !== this.#snapshot.revision)
237
+ this.#snapshot = { revision, servers, tools };
238
+ }
239
+ #refresh(state) {
240
+ if (state.refreshing)
241
+ return state.refreshing;
242
+ if (this.#closed || Date.now() < state.retryAt)
243
+ return Promise.resolve();
244
+ const job = (async () => {
245
+ do {
246
+ state.dirty = false;
247
+ const tools = await this.#enumerate(state);
248
+ if (this.#closed)
249
+ return;
250
+ this.#publish({ state, tools });
251
+ state.retryAt = 0;
252
+ } while (state.dirty && !this.#closed);
253
+ })()
254
+ .catch((error) => {
255
+ state.retryAt = Date.now() + MCP_CATALOG_LIMITS.retryMs;
256
+ throw error;
257
+ })
258
+ .finally(() => {
259
+ delete state.refreshing;
260
+ });
261
+ state.refreshing = job;
262
+ return job;
263
+ }
264
+ async #enumerate(state) {
265
+ if (state.server.transport !== 'HTTP')
266
+ return this.#enumeratePages(state);
267
+ if (this.#httpRefreshActive < MCP_CATALOG_LIMITS.httpConcurrency)
268
+ this.#httpRefreshActive++;
269
+ else
270
+ await new Promise((resolve) => this.#httpWaiters.push(resolve));
271
+ try {
272
+ this.#controller.signal.throwIfAborted();
273
+ return await this.#enumeratePages(state);
274
+ }
275
+ finally {
276
+ const next = this.#httpWaiters.shift();
277
+ if (next)
278
+ next();
279
+ else
280
+ this.#httpRefreshActive--;
281
+ }
282
+ }
283
+ async #enumeratePages(state) {
284
+ const tools = new Map();
285
+ const cursors = new Set();
286
+ let cursor;
287
+ let bytes = 0;
288
+ for (let page = 0; page < MCP_CATALOG_LIMITS.maximumPages; page++) {
289
+ const listed = await state.client.listTools(cursor === undefined ? {} : { cursor }, {
290
+ signal: this.#controller.signal,
291
+ timeout: MCP_CATALOG_LIMITS.timeoutMs
292
+ });
293
+ bytes += Buffer.byteLength(JSON.stringify(listed));
294
+ if (bytes > MCP_CATALOG_LIMITS.maximumBytes)
295
+ throw new Error('MAR_AGENT_MCP_CATALOG_LIMIT');
296
+ for (const tool of listed.tools) {
297
+ const name = `mcp__${safeName(state.server.name)}__${safeName(tool.name)}`;
298
+ if (tools.has(name))
299
+ throw new Error('MAR_AGENT_MCP_TOOL_NAME_CONFLICT');
300
+ const definition = {
301
+ name,
302
+ description: tool.description ?? `MCP tool ${tool.name}.`,
303
+ allowedCallers: [
304
+ ...(state.server.allowedToolCallers?.[tool.name] ??
305
+ (state.server.origin === 'BUILTIN' ? ['direct'] : ['direct', 'programmatic']))
306
+ ],
307
+ inputSchema: tool.inputSchema,
308
+ ...(tool.outputSchema
309
+ ? { outputSchema: tool.outputSchema }
310
+ : {})
311
+ };
312
+ assertMcpToolDefinition(definition);
313
+ const metadata = {
314
+ definition,
315
+ serverId: state.metadata.id,
316
+ rawName: tool.name,
317
+ sourceIdentity: state.sourceIdentity,
318
+ definitionRevision: fingerprint(definition),
319
+ lifetime: state.server.lifecycle === 'EXECUTION' ? 'execution' : 'session',
320
+ exposure: state.server.origin === 'BUILTIN' ? 'direct' : 'deferred',
321
+ ...(tool.annotations ? { annotations: tool.annotations } : {})
322
+ };
323
+ tools.set(name, {
324
+ client: state.client,
325
+ nativeName: tool.name,
326
+ metadata
327
+ });
328
+ if (tools.size > MCP_CATALOG_LIMITS.maximumTools)
329
+ throw new Error('MAR_AGENT_MCP_CATALOG_LIMIT');
330
+ }
331
+ cursor = listed.nextCursor;
332
+ if (cursor === undefined)
333
+ return tools;
334
+ if (cursors.has(cursor))
335
+ throw new Error('MAR_AGENT_MCP_CATALOG_CURSOR_INVALID');
336
+ cursors.add(cursor);
337
+ }
338
+ throw new Error('MAR_AGENT_MCP_CATALOG_LIMIT');
155
339
  }
156
340
  async #connect(server, cwd, options) {
157
341
  const client = new Client({ name: 'maragent', version: '1.0.0' });
342
+ this.#clients.push(client);
343
+ let state;
344
+ let initialDirty = false;
345
+ client.setNotificationHandler(ToolListChangedNotificationSchema, () => {
346
+ if (!state) {
347
+ initialDirty = true;
348
+ return;
349
+ }
350
+ state.dirty = true;
351
+ if (this.#ready)
352
+ void this.#refresh(state).catch(() => undefined);
353
+ });
158
354
  const environment = { ...getDefaultEnvironment(), ...(server.environment ?? {}) };
159
355
  const transport = server.transport === 'STDIO'
160
356
  ? options.processExecutor === undefined
@@ -177,22 +373,49 @@ export class MarAgentMcpClientPool {
177
373
  : new StreamableHTTPClientTransport(new URL(server.url), {
178
374
  requestInit: { headers: { ...(server.headers ?? {}) } }
179
375
  });
180
- await client.connect(transport);
181
- this.#clients.push(client);
182
- const listed = await client.listTools();
183
- for (const tool of listed.tools) {
184
- const name = `mcp__${safeName(server.name)}__${safeName(tool.name)}`;
185
- if (this.#tools.has(name))
186
- throw new Error('MAR_AGENT_MCP_TOOL_NAME_CONFLICT');
187
- this.#tools.set(name, {
188
- client,
189
- nativeName: tool.name,
190
- description: tool.description ?? `MCP tool ${tool.name}.`,
191
- inputSchema: tool.inputSchema
192
- });
193
- }
376
+ await client.connect(transport, {
377
+ signal: this.#controller.signal,
378
+ timeout: MCP_CATALOG_LIMITS.timeoutMs
379
+ });
380
+ const sourceIdentity = fingerprint({
381
+ source: server.sourceIdentity ?? {
382
+ name: server.name,
383
+ transport: server.transport,
384
+ url: server.url,
385
+ command: server.command,
386
+ args: server.args
387
+ },
388
+ cwd,
389
+ allowedToolCallers: server.allowedToolCallers,
390
+ authorizationScope: options.authorizationScope ?? cwd,
391
+ principal: server.authorizationIdentity ?? this.#unknownPrincipal
392
+ });
393
+ const version = client.getServerVersion();
394
+ state = {
395
+ server,
396
+ client,
397
+ sourceIdentity,
398
+ retryAt: 0,
399
+ dirty: initialDirty,
400
+ tools: new Map(),
401
+ metadata: {
402
+ id: sourceIdentity,
403
+ name: server.name,
404
+ ...(version?.title ? { description: version.title } : {}),
405
+ ...(client.getInstructions() ? { instructions: client.getInstructions() } : {})
406
+ }
407
+ };
408
+ state.tools = await this.#enumerate(state);
409
+ if (this.#servers.has(state.metadata.id))
410
+ throw new Error('MAR_AGENT_MCP_SERVER_NAME_CONFLICT');
411
+ this.#servers.set(state.metadata.id, state);
194
412
  }
195
413
  }
414
+ function fingerprint(value) {
415
+ return createHash('sha256')
416
+ .update(JSON.stringify(canonicalValue(value)))
417
+ .digest('hex');
418
+ }
196
419
  function singleUseLease(pool) {
197
420
  let released = false;
198
421
  return {
@@ -205,10 +428,11 @@ function singleUseLease(pool) {
205
428
  }
206
429
  };
207
430
  }
208
- function mcpConfigurationFingerprint(servers, cwd) {
431
+ function mcpConfigurationFingerprint(servers, cwd, authorizationScope) {
209
432
  return createHash('sha256')
210
433
  .update(JSON.stringify(canonicalValue({
211
434
  cwd,
435
+ authorizationScope,
212
436
  inheritedEnvironment: getDefaultEnvironment(),
213
437
  servers
214
438
  })))
@@ -65,7 +65,12 @@ export class MarAgentRuntimeModelFactory {
65
65
  titleGeneration: configuration.titleGeneration,
66
66
  codeMode: configuration.codeMode,
67
67
  highDensityCompaction: configuration.highDensityCompaction,
68
- ...(configuration.fastMode === true ? { fastMode: true } : {}),
68
+ ...(configuration.serviceTier === undefined
69
+ ? {}
70
+ : { serviceTier: configuration.serviceTier }),
71
+ ...(configuration.serviceTier === undefined && configuration.fastMode === true
72
+ ? { fastMode: true }
73
+ : {}),
69
74
  hostedWebSearch: configuration.hostedWebSearch,
70
75
  ...(imageGeneration === undefined ? {} : { imageGeneration }),
71
76
  ...(configuration.responsesTransport === undefined
@@ -110,7 +115,7 @@ export class MarAgentRuntimeModelFactory {
110
115
  }
111
116
  : {}),
112
117
  responsesEncoding: configuration.responsesEncoding ?? 'STANDARD',
113
- codexCredential: true,
118
+ responsesConfigurationUpdates: configuration.responsesConfigurationUpdates === true,
114
119
  credentialProvider: provider
115
120
  };
116
121
  });
@@ -296,6 +296,9 @@ export class MarAgentRunner extends AbstractRunner {
296
296
  await catalog.close().catch(() => undefined);
297
297
  }
298
298
  }
299
+ supportsRollback(session) {
300
+ return session.externalSessionId !== null;
301
+ }
299
302
  async clearNative(session) {
300
303
  if (session.externalSessionId === null)
301
304
  throw new Error('SESSION_CLEAR_UNAVAILABLE');
@@ -116,7 +116,8 @@ export class OpenCodeManagedRunController {
116
116
  mcpSecretEnvironment = parseSecretEnvironment(payload.mcpSecretEnvironment);
117
117
  mcp = this.runner.mcpConfiguration(payload.mcpInstallations, {
118
118
  ...environment,
119
- ...mcpSecretEnvironment
119
+ ...mcpSecretEnvironment,
120
+ MAR_USERNAME: environment['MAR_USERNAME'] ?? ''
120
121
  });
121
122
  attachments = this.host.parseAttachments(payload.attachments);
122
123
  }
@@ -79,8 +79,8 @@ export class NodeRuntimeState {
79
79
  // A first user prompt is only a process-local temporary title. A
80
80
  // Runner/native title can replace it on the next discovery refresh.
81
81
  titleSource: customTitle === null ? 'AUTO' : 'CUSTOM',
82
- lastActivityAt: now,
83
- createdAt: now
82
+ lastActivityAt: input.lastActivityAt ?? now,
83
+ createdAt: input.createdAt ?? now
84
84
  };
85
85
  this.sessions.set(value.id, value);
86
86
  return value;
@@ -384,8 +384,10 @@ export class ContinuousRunCoordinator {
384
384
  if (lease !== undefined)
385
385
  this.environmentLeases.set(nextId, lease);
386
386
  }
387
- removeGoalQueue(sessionId, preservedGoal) {
387
+ removeGoalQueue(sessionId, preservedGoal, preservedSubmissionId) {
388
388
  for (const [id, pending] of this.preparedInputs) {
389
+ if (id === preservedSubmissionId)
390
+ continue;
389
391
  if (pending.sessionId !== sessionId || pending.goal === undefined)
390
392
  continue;
391
393
  if (pending.goal.id === preservedGoal?.id && pending.goal.revision === preservedGoal.revision)
@@ -1,5 +1,4 @@
1
1
  import { discoverNativeSessions, readNativeSession } from '../native-session-history.js';
2
- import { coalesceImageGenerationItems } from '../runner/codex/conversation-parser.js';
3
2
  import { nativeConversationPage } from '../util/runner-native-session-parsers.js';
4
3
  import { isPlainRecord } from '../util/node-operation-parsers.js';
5
4
  import { nodeLog } from '../operational.js';
@@ -152,7 +151,28 @@ export class ConversationHistoryService {
152
151
  0);
153
152
  }
154
153
  async readHistory(session, input, snapshotSequence, readAt) {
155
- const selected = await this.readRunnerHistory(session, input);
154
+ let selected;
155
+ try {
156
+ selected = await this.readRunnerHistory(session, input);
157
+ }
158
+ catch (error) {
159
+ // An unavailable persistent source must not erase accepted live output.
160
+ // Older-page and external-session errors remain visible to the caller.
161
+ if (input.cursor !== undefined ||
162
+ this.options.runtime.getAgentSession(session.id) === undefined)
163
+ throw error;
164
+ const runtimePage = this.options.runtime.listConversationTurns({
165
+ sessionId: session.id,
166
+ ...input
167
+ });
168
+ if (runtimePage.turns.length === 0)
169
+ throw error;
170
+ return this.page(runtimePage.nextCursor, runtimePage.turns, {
171
+ snapshotSequence,
172
+ source: 'runtime',
173
+ readAt
174
+ });
175
+ }
156
176
  if (selected.page?.source === 'native')
157
177
  return this.page(selected.page.nextCursor, selected.page.turns, {
158
178
  snapshotSequence,
@@ -179,7 +199,7 @@ export class ConversationHistoryService {
179
199
  if (narrowed !== undefined)
180
200
  official = { ...narrowed, source: 'official' };
181
201
  }
182
- const officialTurns = await this.attachNativeImages(session, official.turns);
202
+ const officialTurns = official.turns;
183
203
  const runtimeTurnsByOfficialId = new Map();
184
204
  const runtimeSupplementalItemsByOfficialId = new Map();
185
205
  for (const officialTurn of officialTurns) {
@@ -261,7 +281,9 @@ export class ConversationHistoryService {
261
281
  readAt
262
282
  });
263
283
  }
264
- const page = this.options.runtime.listConversationTurns({ sessionId: session.id, ...input });
284
+ const page = this.options.runtime.getAgentSession(session.id) === undefined
285
+ ? { turns: [], nextCursor: null }
286
+ : this.options.runtime.listConversationTurns({ sessionId: session.id, ...input });
265
287
  if (session.externalSessionId !== null &&
266
288
  session.nativeControl === 'EXTERNAL' &&
267
289
  page.turns.length === 0)
@@ -308,6 +330,7 @@ export class ConversationHistoryService {
308
330
  : input, runtimeTurns, (images) => this.options.attachments.registerNative(session, images));
309
331
  return native;
310
332
  },
333
+ session,
311
334
  official: () => this.readOfficial(session, input)
312
335
  });
313
336
  return { page, incompleteNative };
@@ -327,9 +350,7 @@ export class ConversationHistoryService {
327
350
  const history = selected.page;
328
351
  if (history === undefined)
329
352
  return this.page(null, [], { snapshotSequence, source: 'runtime', readAt });
330
- const hydratedTurns = history.source === 'official'
331
- ? await this.attachNativeImages(session, history.turns)
332
- : history.turns;
353
+ const hydratedTurns = history.turns;
333
354
  const turns = hydratedTurns.filter((turn) => latestTurnActivityAt(turn) < beforeAt);
334
355
  return this.page(history.nextCursor === null
335
356
  ? null
@@ -421,45 +442,13 @@ export class ConversationHistoryService {
421
442
  await discoverNativeSessions(session.runner, session.cwd);
422
443
  return readNativeSession(session.runner, session.cwd, session.externalSessionId, page, limit);
423
444
  }
424
- async attachNativeImages(session, turns) {
425
- const startedAt = performance.now();
426
- const history = await this.readNative(session);
427
- const readDurationMs = Math.round(performance.now() - startedAt);
428
- if (readDurationMs >= 250)
429
- nodeLog('native.session.history.attachment-source-read.completed', {
430
- runner: session.runner,
431
- sessionId: session.id,
432
- durationMs: readDurationMs
433
- });
434
- if (history === undefined)
435
- return turns;
436
- const attachments = new Map();
437
- for (const item of history.items) {
438
- if (item.kind !== 'tool_call' || item.imageAttachments === undefined)
439
- continue;
440
- const registered = this.options.attachments.registerNative(session, item.imageAttachments);
441
- if (registered.length > 0)
442
- attachments.set(item.toolUseId, registered);
443
- }
444
- if (attachments.size === 0)
445
- return turns;
446
- return turns.map((turn) => ({
447
- ...turn,
448
- items: coalesceImageGenerationItems(turn.items.map((item) => {
449
- const registered = attachments.get(item.id.slice(item.id.lastIndexOf(':') + 1));
450
- if (registered === undefined || !isPlainRecord(item.payload))
451
- return item;
452
- return { ...item, payload: { ...item.payload, attachments: registered } };
453
- }))
454
- }));
455
- }
456
445
  async readOfficial(session, input) {
457
446
  const runner = this.options.runners.require(session.runner);
458
447
  if (session.externalSessionId === null || !runner.available(this.options.capabilities()))
459
448
  return undefined;
460
449
  const startedAt = performance.now();
461
450
  try {
462
- return await runner.readOfficialConversation(session, input, (nativeTurnId) => this.managedRunnerTurn(session, nativeTurnId));
451
+ return await runner.readOfficialConversation(session, input, (nativeTurnId) => this.managedRunnerTurn(session, nativeTurnId), (images) => this.options.attachments.registerNative(session, images));
463
452
  }
464
453
  finally {
465
454
  const durationMs = Math.round(performance.now() - startedAt);
@@ -63,6 +63,8 @@ export class ExternalSessionResumeService {
63
63
  : { externalSessionId: session.externalSessionId }),
64
64
  nativeControl: 'MAR_MANAGED',
65
65
  cwd: session.cwd,
66
+ createdAt: session.createdAt,
67
+ lastActivityAt: session.lastActivityAt,
66
68
  model: configuration.model,
67
69
  effort: configuration.effort,
68
70
  access: configuration.access,
@@ -519,7 +519,7 @@ export class HostedConversationNodeService {
519
519
  temporaryDirectory,
520
520
  pathMounts,
521
521
  processExecutor: this.processExecutor(paths, version, temporaryDirectory, generatedImagesDirectory, generatedImagesLogicalPath),
522
- mcpServers: mcpServers ?? resolveMcpServers(version, this.options.secretEnvironment ?? process.env),
522
+ mcpServers: (mcpServers ?? resolveMcpServers(version, this.options.secretEnvironment ?? process.env)).map((server) => ({ ...server, authorizationIdentity: `user:${payload.userId}` })),
523
523
  webAccess: version.draft.networkMode !== 'NONE',
524
524
  onEvent: (event) => {
525
525
  if (runtimeEventsAccepted)
@@ -98,6 +98,8 @@ export class NativeSessionProjectionService {
98
98
  externalSessionId: direct.externalSessionId,
99
99
  nativeControl: 'EXTERNAL',
100
100
  cwd: direct.cwd,
101
+ createdAt: direct.createdAt,
102
+ lastActivityAt: direct.lastActivityAt,
101
103
  ...(direct.model === null ? {} : { model: direct.model }),
102
104
  ...(direct.effort === null ? {} : { effort: direct.effort }),
103
105
  ...(direct.access === null ? {} : { access: direct.access }),
@@ -155,8 +155,13 @@ export class NativeSessionWatchService {
155
155
  setTimeout(() => void this.convergeHistory(watch, generation), 0);
156
156
  }
157
157
  for (const lease of leases) {
158
- if (!lease.ready)
159
- lease.pendingEvents.push(event);
158
+ if (!lease.ready) {
159
+ // Observations are useful only to an already watching page, never after a snapshot.
160
+ if (event.category !== 'conversation' ||
161
+ !isPlainRecord(event.payload) ||
162
+ event.payload.performance === undefined)
163
+ lease.pendingEvents.push(event);
164
+ }
160
165
  else
161
166
  this.emitLeaseEvent(lease, event);
162
167
  }
@@ -301,8 +306,12 @@ export class NativeSessionWatchService {
301
306
  if (changed || watch.page === undefined)
302
307
  watch.page = accepted;
303
308
  stage = 'emit_page';
304
- if (completingConvergence && accepted.source === 'runtime')
305
- this.releasePendingEvents(session.id);
309
+ if (completingConvergence && accepted.source === 'runtime') {
310
+ if (changed)
311
+ this.emitSnapshotToLeases(session, accepted, true);
312
+ else
313
+ this.releasePendingEvents(session.id);
314
+ }
306
315
  else if (completingConvergence)
307
316
  this.emitSnapshotToLeases(session, accepted, true);
308
317
  else if (changed)
@@ -1,3 +1,4 @@
1
+ import { runPerformanceSchema } from '@myagentroam/protocol';
1
2
  import { nodeLog } from '../operational.js';
2
3
  import { isPlainRecord, isTerminalRunStatus } from '../util/node-operation-parsers.js';
3
4
  const COALESCE_MS = 200;
@@ -9,6 +10,18 @@ export class RunEventService {
9
10
  this.options = options;
10
11
  }
11
12
  emit(runId, eventType, payload, status) {
13
+ if (eventType === 'run.performance') {
14
+ const run = this.options.runtime.getRun(runId);
15
+ if (run === undefined || !isPlainRecord(payload))
16
+ return;
17
+ const parsed = runPerformanceSchema.safeParse({ ...payload, runId: run.id });
18
+ if (parsed.success)
19
+ this.options.emitWorkbench('conversation', {
20
+ sessionId: run.sessionId,
21
+ performance: parsed.data
22
+ });
23
+ return;
24
+ }
12
25
  if (eventType === 'text.delta' &&
13
26
  status === undefined &&
14
27
  isPlainRecord(payload) &&
@@ -115,7 +115,8 @@ export class SessionCatalogService {
115
115
  (await this.options.projection.resolve(input.sessionId));
116
116
  if (source === undefined)
117
117
  throw new Error('SESSION_NOT_FOUND');
118
- if (source.runner !== 'mar-agent' || source.externalSessionId === null)
118
+ const runner = this.options.runners.require(source.runner);
119
+ if (!runner.supportsRollback(source))
119
120
  throw new Error('SESSION_ROLLBACK_UNAVAILABLE');
120
121
  if (this.options.runtime
121
122
  .listRunsForSession(source.id)
@@ -132,9 +133,7 @@ export class SessionCatalogService {
132
133
  throw new Error('SESSION_ROLLBACK_TARGET_INVALID');
133
134
  const releaseMutation = this.options.runtime.beginSessionHistoryMutation(source.id);
134
135
  try {
135
- const lastActivityAt = await this.options.runners
136
- .require(source.runner)
137
- .rollbackNative(source, input.boundary);
136
+ const lastActivityAt = await runner.rollbackNative(source, input.boundary, turn.rewind?.after);
138
137
  const runtimeSession = this.options.runtime.getAgentSession(source.id);
139
138
  const session = runtimeSession === undefined
140
139
  ? { ...source, lastActivityAt }