@myagentroam/node 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.
@@ -93,6 +93,10 @@ export class OpenCodeServerClient {
93
93
  const client = await this.requireClient();
94
94
  return unwrap(await client.session.list({ directory, limit }));
95
95
  }
96
+ async session(directory, sessionID) {
97
+ const client = await this.requireClient();
98
+ return unwrap(await client.session.get({ directory, sessionID }));
99
+ }
96
100
  async createSession(directory, title) {
97
101
  const client = await this.requireClient();
98
102
  return unwrap(await client.session.create({ directory, ...(title === undefined ? {} : { title }) }));
@@ -102,8 +106,15 @@ export class OpenCodeServerClient {
102
106
  return unwrap(await client.session.fork({ directory, sessionID, messageID }));
103
107
  }
104
108
  async messages(directory, sessionID, limit = 100) {
109
+ return (await this.messagePage(directory, sessionID, { limit })).messages;
110
+ }
111
+ async messagePage(directory, sessionID, input) {
105
112
  const client = await this.requireClient();
106
- return unwrap(await client.session.messages({ directory, sessionID, limit }));
113
+ const result = await client.session.messages({ directory, sessionID, ...input });
114
+ return {
115
+ messages: unwrap(result),
116
+ nextCursor: result.response.headers.get('x-next-cursor') || null
117
+ };
107
118
  }
108
119
  async updateSession(directory, sessionID, title) {
109
120
  const client = await this.requireClient();
@@ -249,7 +249,6 @@ export class MarAgentManagedRunController {
249
249
  if (controller.signal.aborted)
250
250
  return;
251
251
  controller.abort();
252
- titleController.abort();
253
252
  cancelExecution?.();
254
253
  },
255
254
  cancelTitle: () => titleController.abort()
@@ -434,7 +433,16 @@ export class MarAgentManagedRunController {
434
433
  currentModelId: input.model.id,
435
434
  prompt: input.input,
436
435
  signal: titleController.signal
437
- }).catch(() => undefined);
436
+ }).catch((error) => {
437
+ if (!titleController.signal.aborted)
438
+ nodeLog('runner.mar-agent.title.failed', {
439
+ sessionId: session.id,
440
+ runId: input.runId,
441
+ stage: 'generate',
442
+ code: runnerErrorProjection(error, 'MAR_AGENT_TITLE_GENERATION_FAILED').code
443
+ });
444
+ return undefined;
445
+ });
438
446
  runtime.automaticTitleTask = { cancel: () => titleController.abort(), result: titleTask };
439
447
  }
440
448
  this.project(input.runId, event);
@@ -448,7 +456,14 @@ export class MarAgentManagedRunController {
448
456
  expectedTitle: metadata.title,
449
457
  executionId: handle.executionId
450
458
  })
451
- .catch(() => undefined);
459
+ .catch((error) => {
460
+ nodeLog('runner.mar-agent.title.failed', {
461
+ sessionId: session.id,
462
+ runId: input.runId,
463
+ stage: 'persist',
464
+ code: runnerErrorProjection(error, 'MAR_AGENT_TITLE_PERSIST_FAILED').code
465
+ });
466
+ });
452
467
  await this.host.syncTitle(session.id, input.cwd);
453
468
  const status = result.status === 'completed'
454
469
  ? 'SUCCEEDED'
@@ -1,4 +1,6 @@
1
1
  import { marAgentNodeModelSummarySchema, resolveMarAgentDefaultReasoningEffort } from '@myagentroam/protocol';
2
+ import { nodeLog } from '../operational.js';
3
+ import { runnerErrorProjection } from '../util/runner-error.js';
2
4
  import { MarAgentError, openMarAgentSessionCatalog } from '@myagentroam/agent';
3
5
  import { homedir } from 'node:os';
4
6
  import { resolve } from 'node:path';
@@ -231,6 +233,7 @@ export class MarAgentRunner extends AbstractRunner {
231
233
  const executionConfiguration = session.modelId === undefined
232
234
  ? await activeCatalog.executionConfiguration(session.id)
233
235
  : session;
236
+ const title = await marAgentSessionTitle(session, activeCatalog);
234
237
  return [
235
238
  {
236
239
  ...context.createProjection({
@@ -238,8 +241,7 @@ export class MarAgentRunner extends AbstractRunner {
238
241
  runner: this.name,
239
242
  externalSessionId: session.id,
240
243
  cwd: session.workspace,
241
- title: session.title,
242
- titleOrigin: 'NATIVE',
244
+ ...title,
243
245
  lastActivityAt: Date.parse(session.lastActivityAt)
244
246
  }),
245
247
  ...(executionConfiguration?.modelId === undefined
@@ -336,9 +338,9 @@ export class MarAgentRunner extends AbstractRunner {
336
338
  let cursor;
337
339
  do {
338
340
  const page = await catalog.list({ limit: 200, ...(cursor ? { cursor } : {}) });
339
- const title = page.sessions.find((candidate) => candidate.id === nativeSessionId)?.title;
340
- if (title !== undefined)
341
- return title;
341
+ const summary = page.sessions.find((candidate) => candidate.id === nativeSessionId);
342
+ if (summary !== undefined)
343
+ return (await marAgentSessionTitle(summary, catalog)).title;
342
344
  cursor = page.nextCursor;
343
345
  } while (cursor !== undefined);
344
346
  return undefined;
@@ -456,6 +458,24 @@ export class MarAgentRunner extends AbstractRunner {
456
458
  }
457
459
  }
458
460
  }
461
+ async function marAgentSessionTitle(session, catalog) {
462
+ const nativeTitle = session.title.trim();
463
+ if (nativeTitle.length === 0 || /^(?:new|untitled) session$/iu.test(nativeTitle)) {
464
+ try {
465
+ const preview = session.firstUserMessagePreview ?? (await catalog.firstUserMessagePreview(session.id));
466
+ if (preview)
467
+ return { title: preview, titleOrigin: 'PREVIEW' };
468
+ }
469
+ catch (error) {
470
+ nodeLog('runner.mar-agent.title.failed', {
471
+ sessionId: session.id,
472
+ stage: 'preview',
473
+ code: runnerErrorProjection(error, 'MAR_AGENT_TITLE_PREVIEW_FAILED').code
474
+ });
475
+ }
476
+ }
477
+ return { title: session.title, titleOrigin: 'NATIVE' };
478
+ }
459
479
  function firstSequenceForTurn(entries, turnId) {
460
480
  return entries.find((entry) => entry.kind === 'user' ? entry.turnId === turnId : entry.event.turnId === turnId)?.sequence;
461
481
  }
@@ -1,6 +1,32 @@
1
1
  import { boundedConversationItemId, compactRunnerText } from '../codex/conversation-parser.js';
2
2
  import { stripGoalStartPrompt } from '../../service/goal-runtime.js';
3
3
  import { normalizeOpenCodeTool } from './tool-normalizer.js';
4
+ export function openCodeUserMessagePreview(messages) {
5
+ for (const message of messages) {
6
+ const entry = record(message);
7
+ const info = record(entry?.info);
8
+ if (info?.role !== 'user' || typeof info.id !== 'string')
9
+ continue;
10
+ const parts = array(entry?.parts);
11
+ if (parts.some((part) => record(part)?.type === 'compaction'))
12
+ continue;
13
+ const text = parts
14
+ .flatMap((part) => {
15
+ const value = record(part);
16
+ return value?.type === 'text' &&
17
+ value.synthetic !== true &&
18
+ value.ignored !== true &&
19
+ typeof value.text === 'string'
20
+ ? [value.text]
21
+ : [];
22
+ })
23
+ .join('\n');
24
+ const preview = stripGoalStartPrompt(text).trim().replace(/\s+/gu, ' ').slice(0, 160);
25
+ if (preview)
26
+ return preview;
27
+ }
28
+ return undefined;
29
+ }
4
30
  export function openCodeConversationPage(session, messages, input, managedTurn) {
5
31
  const records = messages.flatMap((value) => {
6
32
  const entry = record(value);
@@ -5,11 +5,14 @@ import { AbstractRunner } from './abstract-runner.js';
5
5
  import { isWithinWorkspace } from './codex/conversation-parser.js';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { homedir } from 'node:os';
8
- import { openCodeConversationPage } from './opencode/conversation-parser.js';
8
+ import { openCodeConversationPage, openCodeUserMessagePreview } from './opencode/conversation-parser.js';
9
9
  import { nodeLog } from '../operational.js';
10
+ import { runnerErrorProjection } from '../util/runner-error.js';
10
11
  const PROFILE_ENTRY_TTL_MS = 30 * 60_000;
11
12
  const PROFILE_FAILURE_RETRY_MS = 30_000;
12
13
  const PROFILE_REFRESH_TIMEOUT_MS = 90_000;
14
+ const TITLE_PREVIEW_TTL_MS = 60 * 60_000;
15
+ const MAX_TITLE_PREVIEWS = 256;
13
16
  export class OpenCodeRunner extends AbstractRunner {
14
17
  client;
15
18
  clientFactory;
@@ -24,6 +27,7 @@ export class OpenCodeRunner extends AbstractRunner {
24
27
  profileGeneration = 0;
25
28
  execution = new Map();
26
29
  managedTurns = new Map();
30
+ titlePreviews = new Map();
27
31
  environmentClients = new Map();
28
32
  stoppingClients = new Set();
29
33
  constructor(client = new OpenCodeServerClient(), clientFactory = () => new OpenCodeServerClient()) {
@@ -198,19 +202,91 @@ export class OpenCodeRunner extends AbstractRunner {
198
202
  session.parentId !== undefined ||
199
203
  !isWithinWorkspace(session.directory, target.path))
200
204
  continue;
205
+ const title = await this.sessionTitle(session);
201
206
  result.push(context.createProjection({
202
207
  workspace: target,
203
208
  runner: this.name,
204
209
  externalSessionId: session.id,
205
210
  cwd: session.directory,
206
- title: session.title,
207
- titleOrigin: 'OFFICIAL',
211
+ title: title.title,
212
+ titleOrigin: title.titleOrigin,
208
213
  lastActivityAt: session.updatedAt
209
214
  }));
210
215
  }
211
216
  }
212
217
  return result;
213
218
  }
219
+ async readManagedRunnerTitle(session) {
220
+ if (session.externalSessionId === null)
221
+ return undefined;
222
+ const native = openCodeSession(await this.client.session(session.cwd, session.externalSessionId));
223
+ if (native === undefined ||
224
+ native.id !== session.externalSessionId ||
225
+ !isWithinWorkspace(native.directory, session.cwd))
226
+ return undefined;
227
+ return (await this.sessionTitle(native)).title;
228
+ }
229
+ async sessionTitle(session) {
230
+ const nativeTitle = session.title.trim();
231
+ if (nativeTitle.length === 0 ||
232
+ /^(?:new|untitled) session(?:\s*[-–—:]\s*\d{4}-\d{2}-\d{2}.*)?$/iu.test(nativeTitle)) {
233
+ try {
234
+ const preview = await this.firstUserMessagePreview(session);
235
+ if (preview)
236
+ return { title: preview, titleOrigin: 'PREVIEW' };
237
+ }
238
+ catch (error) {
239
+ nodeLog('runner.opencode.title.failed', {
240
+ sessionId: session.id,
241
+ stage: 'preview',
242
+ code: runnerErrorProjection(error, 'OPENCODE_TITLE_PREVIEW_FAILED').code
243
+ });
244
+ }
245
+ }
246
+ return { title: session.title, titleOrigin: 'OFFICIAL' };
247
+ }
248
+ async firstUserMessagePreview(session) {
249
+ const key = `${session.directory}\0${session.id}`;
250
+ const cached = this.titlePreviews.get(key);
251
+ if (cached !== undefined &&
252
+ cached.updatedAt === session.updatedAt &&
253
+ cached.expiresAt > Date.now())
254
+ return cached.result;
255
+ this.titlePreviews.delete(key);
256
+ if (this.titlePreviews.size >= MAX_TITLE_PREVIEWS)
257
+ this.titlePreviews.delete(this.titlePreviews.keys().next().value);
258
+ const entry = {
259
+ updatedAt: session.updatedAt,
260
+ expiresAt: Date.now() + TITLE_PREVIEW_TTL_MS,
261
+ result: (async () => {
262
+ let before;
263
+ let preview;
264
+ const cursors = new Set();
265
+ for (;;) {
266
+ const page = await this.client.messagePage(session.directory, session.id, {
267
+ limit: 100,
268
+ ...(before === undefined ? {} : { before })
269
+ });
270
+ preview = openCodeUserMessagePreview(page.messages) ?? preview;
271
+ if (page.nextCursor === null)
272
+ return preview;
273
+ if (cursors.has(page.nextCursor))
274
+ throw new Error('OPENCODE_MESSAGE_CURSOR_INVALID');
275
+ cursors.add(page.nextCursor);
276
+ before = page.nextCursor;
277
+ }
278
+ })()
279
+ };
280
+ this.titlePreviews.set(key, entry);
281
+ try {
282
+ return await entry.result;
283
+ }
284
+ catch (error) {
285
+ if (this.titlePreviews.get(key) === entry)
286
+ this.titlePreviews.delete(key);
287
+ throw error;
288
+ }
289
+ }
214
290
  async resumeExternal(external, context) {
215
291
  if (external.runner !== this.name || external.externalSessionId === null || !context.available)
216
292
  return { failureCode: 'RUNNER_UNAVAILABLE' };
@@ -328,6 +404,7 @@ export class OpenCodeRunner extends AbstractRunner {
328
404
  execution.abort.abort();
329
405
  this.execution.clear();
330
406
  this.managedTurns.clear();
407
+ this.titlePreviews.clear();
331
408
  this.profileCache = undefined;
332
409
  this.profileRefresh = undefined;
333
410
  if (this.profileRefreshTimer !== undefined)
@@ -268,14 +268,16 @@ export class NodeRuntimeState {
268
268
  });
269
269
  }
270
270
  /**
271
- * Give a just-created managed Session a readable local title before the
272
- * Runner has written its own transcript title. This intentionally never
273
- * replaces a user rename or a Runner-provided title.
271
+ * Give a new or restored managed Session a readable local title while the
272
+ * Runner still has a placeholder. Never replace a user rename or a
273
+ * non-placeholder Runner title.
274
274
  */
275
275
  setTemporaryTitleIfMissing(id, title) {
276
276
  const current = this.requireSession(id);
277
277
  const value = title.trim().replace(/\s+/g, ' ').slice(0, 160);
278
- if (value.length === 0 || current.customTitle !== null || current.runnerTitle !== null)
278
+ if (value.length === 0 ||
279
+ current.customTitle !== null ||
280
+ (current.runnerTitle !== null && !isDefaultRunnerTitle(current.runnerTitle)))
279
281
  return current;
280
282
  return this.replaceSession(id, { runnerTitle: value, titleSource: 'AUTO' });
281
283
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.157",
3
+ "version": "0.9.158",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -36,10 +36,10 @@
36
36
  "playwright": "1.62.1",
37
37
  "ws": "^8.21.3",
38
38
  "zod": "4.4.3",
39
- "@myagentroam/agent": "0.9.157",
40
39
  "@myagentroam/code-navigation": "0.9.129",
41
- "@myagentroam/marctl": "0.9.120",
42
- "@myagentroam/protocol": "0.9.157"
40
+ "@myagentroam/agent": "0.9.158",
41
+ "@myagentroam/protocol": "0.9.158",
42
+ "@myagentroam/marctl": "0.9.120"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/ws": "^8.18.1"