@myagentroam/node 0.11.18 → 0.11.20

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.
@@ -60,6 +60,7 @@ export function unavailableCapabilities() {
60
60
  promptCompletion: { apiVersion: 1, contextVersion: 3, automaticTriggers: true },
61
61
  chatgptLogin: { apiVersion: 1, loopback: true },
62
62
  workspaceDirectTransfer: workspaceDirectTransferCapability(),
63
+ sessionGroups: { apiVersion: 1 },
63
64
  terminalDirect: terminalDirectCapability()
64
65
  };
65
66
  }
@@ -96,6 +97,7 @@ export function detectCapabilities(commands = {}) {
96
97
  promptCompletion: { apiVersion: 1, contextVersion: 3, automaticTriggers: true },
97
98
  chatgptLogin: { apiVersion: 1, loopback: true },
98
99
  workspaceDirectTransfer: workspaceDirectTransferCapability(),
100
+ sessionGroups: { apiVersion: 1 },
99
101
  terminalDirect: terminalDirectCapability()
100
102
  };
101
103
  }
@@ -136,6 +138,7 @@ export async function detectCapabilitiesAsync(commands = {}) {
136
138
  promptCompletion: { apiVersion: 1, contextVersion: 3, automaticTriggers: true },
137
139
  chatgptLogin: { apiVersion: 1, loopback: true },
138
140
  workspaceDirectTransfer: workspaceDirectTransferCapability(),
141
+ sessionGroups: { apiVersion: 1 },
139
142
  terminalDirect: terminalDirectCapability()
140
143
  };
141
144
  }
package/dist/connector.js CHANGED
@@ -46,6 +46,7 @@ import { WorkspaceSessionService } from './service/workspace-session-service.js'
46
46
  import { WorkbenchEventService } from './service/workbench-event-service.js';
47
47
  import { RunDomainState } from './service/run-domain-state.js';
48
48
  import { RecentRunProjectionService } from './service/recent-run-projection-service.js';
49
+ import { sessionGroupChangedSchema } from '@myagentroam/protocol';
49
50
  import { SessionDomainState } from './service/session-domain-state.js';
50
51
  import { RunEventService } from './service/run-event-service.js';
51
52
  import { RunAttachmentService } from './service/run-attachment-service.js';
@@ -79,7 +80,7 @@ import { MailNotificationControl } from './service/mail-notification-control.js'
79
80
  import { NotificationMcpBridge, resolveNotificationCapability } from './service/notification-mcp-bridge.js';
80
81
  import { SessionCatalogService } from './service/session-catalog-service.js';
81
82
  import { SessionCommandService } from './service/session-command-service.js';
82
- import { SessionPresentationService } from './service/session-presentation-service.js';
83
+ import { SessionPresentationService, sessionGroup } from './service/session-presentation-service.js';
83
84
  import { isPlainRecord, isRunnerImageAttachment, parseRunnerImageAttachments } from './util/node-operation-parsers.js';
84
85
  import { WebToolService } from './service/web-tool-service.js';
85
86
  export { nodeConnectEndpoint, validatedCapabilities } from './util/node-operation-parsers.js';
@@ -119,6 +120,7 @@ const CONVERSATION_TEMPLATE_OPERATIONS = [
119
120
  ];
120
121
  const HOSTED_CONVERSATION_OPERATIONS = [
121
122
  'conversation.hosted.list',
123
+ 'conversation.hosted.model-options',
122
124
  'conversation.hosted.create',
123
125
  'conversation.hosted.get',
124
126
  'conversation.hosted.history',
@@ -172,6 +174,7 @@ export class NodeConnector {
172
174
  restartAuthorization;
173
175
  restartHandoff;
174
176
  inFlightOperations = new Set();
177
+ pendingSessionGroups = new Map();
175
178
  mailNotificationControl = new MailNotificationControl((envelope) => this.sendEnvelope(envelope));
176
179
  scheduledSessions;
177
180
  nodeRequests;
@@ -907,6 +910,17 @@ export class NodeConnector {
907
910
  emitTurn: (turn) => this.emitConversationTurn(turn)
908
911
  });
909
912
  this.sessionLifecycleService = new SessionLifecycleService({
913
+ parentCleared: (session) => {
914
+ this.recentRunProjectionService.clearSessionParent(session.workspaceId, session.id);
915
+ const change = {
916
+ nodeId: session.nodeId,
917
+ workspaceId: session.workspaceId,
918
+ sessionId: session.id,
919
+ sessionGroup: 'MAIN'
920
+ };
921
+ this.pendingSessionGroups.set(session.id, change);
922
+ this.send('session.group.changed', change);
923
+ },
910
924
  database: () => {
911
925
  if (this.database === undefined)
912
926
  throw new Error('NODE_DATABASE_UNAVAILABLE');
@@ -1176,8 +1190,19 @@ export class NodeConnector {
1176
1190
  await this.connectionLifecycle.register(nodeId, credential);
1177
1191
  this.mailNotificationControl.setFeatures(payload);
1178
1192
  this.usageOutbox?.connected(payload);
1193
+ for (const change of this.pendingSessionGroups.values())
1194
+ this.send('session.group.changed', change);
1179
1195
  },
1180
1196
  usageAck: (payload) => this.usageOutbox?.handle(createEnvelope('usage.ack', payload)),
1197
+ sessionGroupAck: (payload) => {
1198
+ const parsed = sessionGroupChangedSchema.safeParse(payload);
1199
+ if (!parsed.success)
1200
+ return;
1201
+ const pending = this.pendingSessionGroups.get(parsed.data.sessionId);
1202
+ if (pending?.nodeId === parsed.data.nodeId &&
1203
+ pending.workspaceId === parsed.data.workspaceId)
1204
+ this.pendingSessionGroups.delete(parsed.data.sessionId);
1205
+ },
1181
1206
  bindUsage: (payload) => {
1182
1207
  if (!isPlainRecord(payload) ||
1183
1208
  typeof payload.runId !== 'string' ||
@@ -1189,6 +1214,8 @@ export class NodeConnector {
1189
1214
  },
1190
1215
  connected: (payload) => {
1191
1216
  this.usageOutbox?.connected(payload);
1217
+ for (const change of this.pendingSessionGroups.values())
1218
+ this.send('session.group.changed', change);
1192
1219
  this.connectionLifecycle.connected();
1193
1220
  this.mailNotificationControl.setFeatures(payload);
1194
1221
  },
@@ -1326,6 +1353,9 @@ export class NodeConnector {
1326
1353
  const session = this.runtime.getAgentSession(sessionId);
1327
1354
  return session?.customTitle ?? session?.runnerTitle ?? sessionId;
1328
1355
  },
1356
+ sessionGroup: (task) => sessionGroup(this.runtime.getAgentSession(task.sessionId) ??
1357
+ this.database?.getNativeSessionMetadata(task.runner, task.sessionId, task.workspaceId) ??
1358
+ {}),
1329
1359
  emitNotification: (payload) => this.emitWorkbenchEvent('conversation', payload)
1330
1360
  });
1331
1361
  this.runnerService = new RunnerService(requireDatabase, () => this.capabilities, this.runners, this.runnerUsageReader);
@@ -1357,7 +1387,11 @@ export class NodeConnector {
1357
1387
  files: this.workspaceFileService,
1358
1388
  uploads: this.workspaceUploadService,
1359
1389
  changes: this.workspaceChangeService,
1360
- listSessions: (workspaceId) => this.executeNodeOperation('session.list', { workspaceId, limit: 30 }),
1390
+ listSessions: (workspaceId, sessionGroup) => this.executeNodeOperation('session.list', {
1391
+ workspaceId,
1392
+ limit: 30,
1393
+ ...(sessionGroup === undefined ? {} : { sessionGroup })
1394
+ }),
1361
1395
  invalidate: this.invalidateWorkspaceFiles,
1362
1396
  nodeId: () => this.config?.nodeId ?? 'runtime-node',
1363
1397
  nodeGeneration: () => this.nodeGeneration,
package/dist/database.js CHANGED
@@ -281,6 +281,13 @@ export class NodeDatabase {
281
281
  pinOrder: nullableNumber(row, 'pin_order')
282
282
  };
283
283
  }
284
+ clearSessionParent(runner, externalSessionId, workspaceId) {
285
+ const result = this.raw
286
+ .prepare("UPDATE agent_sessions SET metadata = json_set(metadata, '$.parentSessionId', NULL) WHERE id = ? AND runner = ? AND workspace_id = ?")
287
+ .run(externalSessionId, runner, workspaceId);
288
+ if (result.changes !== 1)
289
+ throw new Error('SESSION_NOT_FOUND');
290
+ }
284
291
  saveNativeSessionMetadata(input) {
285
292
  const current = this.getNativeSessionMetadata(input.runner, input.externalSessionId, input.workspaceId);
286
293
  const customTitle = input.customTitle === undefined
@@ -308,7 +315,7 @@ export class NodeDatabase {
308
315
  const model = input.model === undefined ? (current?.model ?? null) : input.model;
309
316
  const effort = input.effort === undefined ? (current?.effort ?? null) : input.effort;
310
317
  const access = input.access === undefined ? (current?.access ?? null) : input.access;
311
- const parentSessionId = current?.parentSessionId ?? input.parentSessionId ?? null;
318
+ const parentSessionId = current === undefined ? (input.parentSessionId ?? null) : (current.parentSessionId ?? null);
312
319
  const metadata = JSON.stringify({
313
320
  version: 1,
314
321
  parentSessionId,
@@ -343,6 +343,9 @@ export class NodeRuntimeState {
343
343
  titleSource: title === null ? 'AUTO' : 'CUSTOM'
344
344
  });
345
345
  }
346
+ clearSessionParent(sessionId) {
347
+ return this.replaceSession(this.requireSession(sessionId).id, { parentSessionId: null });
348
+ }
346
349
  /**
347
350
  * Give a new or restored managed Session a readable local title while the
348
351
  * Runner still has a placeholder. Never replace a user rename or a
@@ -4,7 +4,7 @@ import { randomUUID } from 'node:crypto';
4
4
  import { constants } from 'node:fs';
5
5
  import { lstat, mkdir, open, readFile, rename, rm, utimes } from 'node:fs/promises';
6
6
  import { join } from 'node:path';
7
- import { toolResultResourceInputSchema, toolResultResourcePageSchema, conversationTurnSchema, conversationTemplateSummarySchema, hostedConversationConfigUpdateInputSchema, hostedConversationCreateInputSchema, hostedConversationDeleteResultSchema, hostedConversationFileDeleteInputSchema, hostedConversationFileDeleteResultSchema, hostedConversationFileReadInputSchema, hostedConversationFileReadResultSchema, hostedConversationFilesSchema, hostedConversationHistoryInputSchema, hostedConversationHistoryPageSchema, hostedConversationIdInputSchema, hostedConversationImageReadInputSchema, hostedConversationListInputSchema, hostedConversationListPageSchema, hostedConversationMessageInputSchema, hostedConversationMetadataUpdateInputSchema, hostedConversationRollbackInputSchema, hostedConversationSnapshotSchema, hostedConversationSummarySchema, hostedConversationUnwatchInputSchema, hostedConversationUploadChunkInputSchema, hostedConversationUploadControlInputSchema, hostedConversationUploadCreateInputSchema, hostedConversationUploadPreflightInputSchema, hostedConversationUploadPreflightResultSchema, hostedConversationUploadSchema, hostedConversationWatchInputSchema, marAgentRuntimeModelConfigurationSchema, resolveMarAgentDefaultReasoningEffort } from '@myagentroam/protocol';
7
+ import { toolResultResourceInputSchema, toolResultResourcePageSchema, conversationTurnSchema, conversationTemplateSummarySchema, hostedConversationConfigUpdateInputSchema, hostedConversationCreateInputSchema, hostedConversationDeleteResultSchema, hostedConversationFileDeleteInputSchema, hostedConversationFileDeleteResultSchema, hostedConversationFileReadInputSchema, hostedConversationFileReadResultSchema, hostedConversationFilesSchema, hostedConversationHistoryInputSchema, hostedConversationHistoryPageSchema, hostedConversationIdInputSchema, hostedConversationImageReadInputSchema, hostedConversationListInputSchema, hostedConversationListPageSchema, hostedConversationMessageInputSchema, hostedConversationMetadataUpdateInputSchema, hostedConversationModelOptionsInputSchema, hostedConversationModelOptionsSchema, hostedConversationRollbackInputSchema, hostedConversationSnapshotSchema, hostedConversationSummarySchema, hostedConversationUnwatchInputSchema, hostedConversationUploadChunkInputSchema, hostedConversationUploadControlInputSchema, hostedConversationUploadCreateInputSchema, hostedConversationUploadPreflightInputSchema, hostedConversationUploadPreflightResultSchema, hostedConversationUploadSchema, hostedConversationWatchInputSchema, marAgentRuntimeModelConfigurationSchema, resolveMarAgentDefaultReasoningEffort } from '@myagentroam/protocol';
8
8
  import { hostedConversationGeneratedImagesDisplayDirectory, hostedConversationImageRunId, marAgentGeneratedImagesDirectory } from '../runner/mar-agent/generated-images.js';
9
9
  import { sessionTitleFromFirstMessage } from '../util/node-operation-parsers.js';
10
10
  import { safeErrorCode } from '../util/safe-error.js';
@@ -50,6 +50,7 @@ export class HostedConversationNodeService {
50
50
  operations() {
51
51
  return {
52
52
  'conversation.hosted.list': (raw) => this.list(raw),
53
+ 'conversation.hosted.model-options': (raw) => this.modelOptions(raw),
53
54
  'conversation.hosted.create': (raw) => this.create(raw),
54
55
  'conversation.hosted.get': (raw) => this.get(raw),
55
56
  'conversation.hosted.history': (raw) => this.history(raw),
@@ -106,6 +107,26 @@ export class HostedConversationNodeService {
106
107
  }))
107
108
  });
108
109
  }
110
+ async modelOptions(raw) {
111
+ if (this.disablePrepared)
112
+ throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
113
+ const payload = hostedRequest(raw);
114
+ const input = hostedConversationModelOptionsInputSchema.parse(payload.data);
115
+ const template = conversationTemplateSummarySchema.parse(payload.template);
116
+ if (template.id !== input.templateId ||
117
+ template.nodeId !== this.options.nodeId() ||
118
+ !template.available ||
119
+ template.status !== 'ACTIVE' ||
120
+ template.currentVersionId === null)
121
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_UNAVAILABLE');
122
+ const version = await this.loadTemplateVersion(template.currentVersionId, template.id);
123
+ const models = allowedModels(version, runtimeModels(payload.models)).filter((model) => model.conversationModel !== false);
124
+ const selected = models.length === 0 ? undefined : selectModel(version, models, version.modelPolicy.defaultId);
125
+ return hostedConversationModelOptionsSchema.parse({
126
+ models: networkModels(models, version.draft.networkMode).map(modelSummary),
127
+ defaultModelId: selected?.id ?? null
128
+ });
129
+ }
109
130
  create(raw) {
110
131
  if (this.disablePrepared)
111
132
  return Promise.reject(new Error('CONVERSATION_HOSTING_UNAVAILABLE'));
@@ -126,15 +147,18 @@ export class HostedConversationNodeService {
126
147
  const existing = this.options.store.findConversationByClientRequest(payload.userId, input.clientRequestId);
127
148
  if (existing !== undefined)
128
149
  return this.snapshot(payload.userId, existing.id, models);
129
- const operation = this.createConversation(payload.userId, input.clientRequestId, template, models, payload.runtimeScopeId);
150
+ const operation = this.createConversation(payload.userId, input.clientRequestId, template, models, input.modelId, payload.runtimeScopeId);
130
151
  this.pendingCreates.set(key, operation);
131
152
  void operation.finally(() => this.pendingCreates.delete(key)).catch(() => undefined);
132
153
  return operation;
133
154
  }
134
- async createConversation(userId, clientRequestId, template, models, runtimeScopeId) {
155
+ async createConversation(userId, clientRequestId, template, models, requestedModelId, runtimeScopeId) {
135
156
  await this.options.assertCanCreate?.();
136
157
  const version = await this.loadTemplateVersion(template.currentVersionId, template.id);
137
- const selected = selectModel(version, models, version.modelPolicy.defaultId);
158
+ if (requestedModelId !== undefined &&
159
+ !allowedModels(version, models).some((model) => model.id === requestedModelId && model.conversationModel !== false))
160
+ throw new Error('HOSTED_CONVERSATION_MODEL_NOT_AVAILABLE');
161
+ const selected = selectModel(version, models, requestedModelId ?? version.modelPolicy.defaultId);
138
162
  const conversationId = this.id();
139
163
  const prepared = await this.options.store.prepareConversationEnvironment(userId, conversationId);
140
164
  const pathMounts = this.pathMounts(prepared.paths, version);
@@ -51,6 +51,10 @@ export class NodeControlMessageService {
51
51
  }
52
52
  if (envelope.type === 'ack')
53
53
  return;
54
+ if (envelope.type === 'session.group.ack') {
55
+ this.options.sessionGroupAck?.(envelope.payload);
56
+ return;
57
+ }
54
58
  if (envelope.type === 'run.approval.decision') {
55
59
  this.decideApproval(envelope);
56
60
  return;
@@ -7,6 +7,7 @@ import { tmpdir } from 'node:os';
7
7
  import { join } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { notificationInputSchema, notificationChannelsRequestSchema, mailNotificationRequestSchema } from '@myagentroam/protocol';
10
+ import { sessionGroup } from './session-presentation-service.js';
10
11
  export class NotificationMcpBridge {
11
12
  resolve;
12
13
  send;
@@ -177,7 +178,7 @@ export function resolveNotificationCapability(runtime, capability) {
177
178
  run.initiatedByUserId !== capability.userId ||
178
179
  (run.source === 'SCHEDULED' ? 'SCHEDULED' : 'MANUAL') !== capability.source)
179
180
  return undefined;
180
- return { ...capability, sessionId: session.id };
181
+ return { ...capability, sessionId: session.id, sessionGroup: sessionGroup(session) };
181
182
  }
182
183
  function notificationMcpStdioArgs() {
183
184
  const javascript = fileURLToPath(new URL('./notification-mcp-stdio.js', import.meta.url));
@@ -38,7 +38,8 @@ export class PromptCompletionService {
38
38
  finally {
39
39
  const context = JSON.stringify(analysis);
40
40
  try {
41
- await this.options.record?.({
41
+ // Keep persistence on the journal's serial queue, outside the response path.
42
+ const recording = this.options.record?.({
42
43
  schemaVersion: 1,
43
44
  startedAt,
44
45
  finishedAt: this.now(),
@@ -63,6 +64,9 @@ export class PromptCompletionService {
63
64
  status: failed ? 'failed' : result?.status,
64
65
  reason: failed ? failureCode : result?.status === 'skipped' ? result.reason : undefined
65
66
  });
67
+ void recording?.catch(() => {
68
+ console.warn('PROMPT_COMPLETION_JOURNAL_WRITE_FAILED');
69
+ });
66
70
  }
67
71
  catch {
68
72
  console.warn('PROMPT_COMPLETION_JOURNAL_WRITE_FAILED');
@@ -1,3 +1,4 @@
1
+ import { sessionGroup } from './session-presentation-service.js';
1
2
  import { composerVisibleText } from '@myagentroam/protocol';
2
3
  const TERMINAL_TTL_MS = 12 * 60 * 60_000;
3
4
  const CLEANUP_INTERVAL_MS = 5 * 60_000;
@@ -36,6 +37,7 @@ export class RecentRunProjectionService {
36
37
  workspaceId: run.workspaceId,
37
38
  sessionId: run.sessionId,
38
39
  sessionTitle: session.customTitle ?? session.runnerTitle ?? session.id,
40
+ sessionGroup: sessionGroup(session),
39
41
  ...(latestUserMessage === undefined ? {} : { userMessage: latestUserMessage }),
40
42
  runner: run.runner,
41
43
  status: run.status,
@@ -55,6 +57,11 @@ export class RecentRunProjectionService {
55
57
  this.cleanup();
56
58
  return [...this.runsBySession.values()].sort(compareRecentRuns);
57
59
  }
60
+ clearSessionParent(workspaceId, sessionId) {
61
+ for (const [key, run] of this.runsBySession)
62
+ if (run.workspaceId === workspaceId && run.sessionId === sessionId)
63
+ this.runsBySession.set(key, { ...run, sessionGroup: 'MAIN' });
64
+ }
58
65
  publishSnapshot() {
59
66
  this.emit({ kind: 'SNAPSHOT', runs: [...this.snapshot()] });
60
67
  }
@@ -1,3 +1,4 @@
1
+ import { sessionGroup } from './session-presentation-service.js';
1
2
  import { permissionStopSchema, runPerformanceSchema } from '@myagentroam/protocol';
2
3
  import { nodeLog } from '../operational.js';
3
4
  import { isPlainRecord, isTerminalRunStatus } from '../util/node-operation-parsers.js';
@@ -252,6 +253,7 @@ export class RunEventService {
252
253
  workspaceId: run.workspaceId,
253
254
  sessionId: run.sessionId,
254
255
  sessionTitle: session.customTitle ?? session.runnerTitle ?? session.id,
256
+ sessionGroup: sessionGroup(session),
255
257
  runId
256
258
  };
257
259
  if (!Number.isSafeInteger(base.userId))
@@ -41,6 +41,7 @@ export class ScheduledSessionCoordinator {
41
41
  eventType: 'notification',
42
42
  payload: {
43
43
  targetType: 'WORKSPACE_SESSION',
44
+ sessionGroup: this.options.sessionGroup?.(task) ?? 'MAIN',
44
45
  userId: currentTask.ownerUserId,
45
46
  workspaceId: task.workspaceId,
46
47
  sessionId: task.sessionId,
@@ -68,6 +69,7 @@ export class ScheduledSessionCoordinator {
68
69
  eventType: 'notification',
69
70
  payload: {
70
71
  targetType: 'WORKSPACE_SESSION',
72
+ sessionGroup: this.options.sessionGroup?.(task) ?? 'MAIN',
71
73
  userId: task.ownerUserId,
72
74
  workspaceId: task.workspaceId,
73
75
  sessionId,
@@ -1,3 +1,4 @@
1
+ import { sessionGroup } from './session-presentation-service.js';
1
2
  import { removeNativeSession } from '../native-session-history.js';
2
3
  import { nodeLog } from '../operational.js';
3
4
  import { isTerminalRunStatus, sessionPage } from '../util/node-operation-parsers.js';
@@ -20,15 +21,33 @@ export class SessionCatalogService {
20
21
  };
21
22
  }
22
23
  async list(input) {
24
+ if (input.sessionGroup !== undefined &&
25
+ input.sessionGroup !== 'MAIN' &&
26
+ input.sessionGroup !== 'CHILD')
27
+ throw new Error('SESSION_INVALID');
23
28
  const sessions = await this.collect(input);
24
- const page = sessionPage(sessions.filter((session) => input.parentSessionId === undefined || session.parentSessionId === input.parentSessionId), input);
29
+ const runningCounts = { MAIN: 0, CHILD: 0 };
30
+ for (const session of sessions)
31
+ if ('activity' in session && session.activity === 'MANAGED_ACTIVE')
32
+ runningCounts[sessionGroup(session)] += 1;
33
+ const query = typeof input.query === 'string'
34
+ ? input.query.trim().normalize('NFKC').toLocaleLowerCase()
35
+ : '';
36
+ const page = sessionPage(sessions.filter((session) => (input.parentSessionId === undefined ||
37
+ session.parentSessionId === input.parentSessionId) &&
38
+ (input.sessionGroup === undefined || sessionGroup(session) === input.sessionGroup) &&
39
+ (query.length === 0 ||
40
+ (session.customTitle ?? session.runnerTitle ?? '')
41
+ .normalize('NFKC')
42
+ .toLocaleLowerCase()
43
+ .includes(query))), input);
25
44
  return input.view === 'marctl'
26
45
  ? {
27
46
  view: 'marctl',
28
47
  sessions: page.sessions.map((session) => marctlSessionSummary(this.options.runtime, session)),
29
48
  nextCursor: page.nextCursor
30
49
  }
31
- : page;
50
+ : { ...page, runningCounts };
32
51
  }
33
52
  async graph(input) {
34
53
  if (typeof input.workspaceId !== 'string')
@@ -125,14 +144,7 @@ export class SessionCatalogService {
125
144
  ].map((session) => [session.id, session]));
126
145
  if (workspaceId !== undefined)
127
146
  this.mergePersisted(merged, workspaceId, !registeredOnly && query === undefined && this.options.discoveryPending(workspaceId));
128
- const sessions = [...merged.values()].filter((session) => {
129
- if (query === undefined)
130
- return true;
131
- const title = (session.customTitle ?? session.runnerTitle ?? '')
132
- .normalize('NFKC')
133
- .toLocaleLowerCase();
134
- return title.includes(query);
135
- });
147
+ const sessions = [...merged.values()];
136
148
  return workspaceId === undefined
137
149
  ? sessions
138
150
  : sessions.filter((session) => session.workspaceId === workspaceId);
@@ -24,6 +24,13 @@ export class SessionLifecycleService {
24
24
  this.starts.clear();
25
25
  }
26
26
  async updateMetadata(input) {
27
+ if (input.parentSessionId !== undefined ||
28
+ (input.clearParent !== undefined &&
29
+ (input.clearParent !== true ||
30
+ input.customTitle !== undefined ||
31
+ input.pinned !== undefined ||
32
+ input.sessionPrompt !== undefined)))
33
+ throw new Error('SESSION_INVALID');
27
34
  const sessionPrompt = input.sessionPrompt === undefined ? undefined : normalizeSessionPrompt(input.sessionPrompt);
28
35
  if (typeof input.sessionId !== 'string' ||
29
36
  (input.customTitle !== undefined &&
@@ -39,6 +46,27 @@ export class SessionLifecycleService {
39
46
  (await this.options.projection.createMetadataProjection(input.sessionId));
40
47
  if (current === undefined)
41
48
  throw new Error('SESSION_NOT_FOUND');
49
+ if (input.clearParent === true) {
50
+ if (current.externalSessionId === null)
51
+ throw new Error('SESSION_ACTIVATION_REQUIRED');
52
+ const metadata = this.options
53
+ .database()
54
+ .getNativeSessionMetadata(current.runner, current.externalSessionId, current.workspaceId);
55
+ const parentSessionId = metadata === undefined ? current.parentSessionId : metadata.parentSessionId;
56
+ if (parentSessionId == null) {
57
+ const next = current.parentSessionId == null
58
+ ? current
59
+ : this.options.runtime.clearSessionParent(current.id);
60
+ return { session: this.options.present(next) };
61
+ }
62
+ this.options
63
+ .database()
64
+ .clearSessionParent(current.runner, current.externalSessionId, current.workspaceId);
65
+ const next = this.options.runtime.clearSessionParent(current.id);
66
+ this.options.parentCleared?.(next);
67
+ this.options.emitSession(next);
68
+ return { session: this.options.present(next) };
69
+ }
42
70
  if (sessionPrompt !== undefined && current.externalSessionId === null)
43
71
  throw new Error('SESSION_ACTIVATION_REQUIRED');
44
72
  if (input.pinned === false) {
@@ -12,7 +12,7 @@ export class SessionPresentationService {
12
12
  session = {
13
13
  ...session,
14
14
  sessionPrompt: metadata.sessionPrompt ?? null,
15
- parentSessionId: metadata.parentSessionId ?? session.parentSessionId ?? null,
15
+ parentSessionId: metadata.parentSessionId ?? null,
16
16
  pinnedAt: metadata.pinnedAt,
17
17
  pinOrder: metadata.pinOrder
18
18
  };
@@ -26,6 +26,7 @@ export class SessionPresentationService {
26
26
  return {
27
27
  ...session,
28
28
  parentSessionId: session.parentSessionId ?? null,
29
+ sessionGroup: sessionGroup(session),
29
30
  ...presentation,
30
31
  runtime: {
31
32
  ...presentation.runtime,
@@ -60,6 +61,9 @@ export class SessionPresentationService {
60
61
  });
61
62
  }
62
63
  }
64
+ export function sessionGroup(session) {
65
+ return session.parentSessionId ? 'CHILD' : 'MAIN';
66
+ }
63
67
  export function latestSessionActivityAt(current, discovered) {
64
68
  const known = [current, discovered].filter((value) => Number.isFinite(value) && value > 0);
65
69
  return known.length === 0 ? 0 : Math.max(...known);
@@ -48,6 +48,7 @@ export class WorkbenchManifestService {
48
48
  },
49
49
  directTransfer: capabilities.workspaceDirectTransfer,
50
50
  terminalDirect: capabilities.terminalDirect,
51
+ sessionGroups: capabilities.sessionGroups,
51
52
  git: {
52
53
  available: true,
53
54
  reasonCode: null,
@@ -17,6 +17,9 @@ export class WorkspaceWatchService {
17
17
  if (!plain(input.topics))
18
18
  throw new Error('WORKSPACE_WATCH_INVALID');
19
19
  const sessions = input.topics.sessions === true;
20
+ const sessionGroup = input.topics.sessionGroup;
21
+ if (sessionGroup !== undefined && sessionGroup !== 'MAIN' && sessionGroup !== 'CHILD')
22
+ throw new Error('WORKSPACE_WATCH_INVALID');
20
23
  const changes = input.topics.changes === true;
21
24
  const rawFiles = input.topics.files;
22
25
  let files;
@@ -38,7 +41,7 @@ export class WorkspaceWatchService {
38
41
  }
39
42
  if (!sessions && !changes && files === undefined)
40
43
  throw new Error('WORKSPACE_WATCH_INVALID');
41
- return { sessions, changes, files };
44
+ return { sessions, changes, files, ...(sessionGroup === undefined ? {} : { sessionGroup }) };
42
45
  }
43
46
  key(input) {
44
47
  return `${this.sessionHash(input)}:${this.connectionId(input)}`;
@@ -123,7 +123,7 @@ export class WorkspaceWatchSnapshotService {
123
123
  const subscribers = this.subscribers(workspaceId, topic);
124
124
  if (subscribers.length === 0)
125
125
  return;
126
- if (topic !== 'files') {
126
+ if (topic === 'changes') {
127
127
  const first = subscribers[0];
128
128
  if (first === undefined)
129
129
  return;
@@ -154,7 +154,9 @@ export class WorkspaceWatchSnapshotService {
154
154
  }
155
155
  const views = new Map();
156
156
  for (const watch of subscribers) {
157
- const key = JSON.stringify(watch.topics.files);
157
+ const key = topic === 'files'
158
+ ? JSON.stringify(watch.topics.files)
159
+ : (watch.topics.sessionGroup ?? 'ALL');
158
160
  views.set(key, [...(views.get(key) ?? []), watch]);
159
161
  }
160
162
  await Promise.all([...views.values()].map(async (viewSubscribers) => {
@@ -180,12 +182,12 @@ export class WorkspaceWatchSnapshotService {
180
182
  read(workspace, topic, watch) {
181
183
  const key = topic === 'files'
182
184
  ? `${workspace.id}:files:${JSON.stringify(watch.topics.files)}`
183
- : `${workspace.id}:${topic}:${this.readRevision(workspace.id, topic)}`;
185
+ : `${workspace.id}:${topic}:${watch.topics.sessionGroup ?? 'ALL'}:${this.readRevision(workspace.id, topic)}`;
184
186
  const active = this.reads.get(key);
185
187
  if (active !== undefined)
186
188
  return active;
187
189
  const read = (topic === 'sessions'
188
- ? this.options.listSessions(workspace.id)
190
+ ? this.options.listSessions(workspace.id, watch.topics.sessionGroup)
189
191
  : topic === 'changes'
190
192
  ? workspace.kind === 'GIT_WORKSPACE'
191
193
  ? this.options.changes.current(workspace)
@@ -214,7 +216,7 @@ export class WorkspaceWatchSnapshotService {
214
216
  viewKey(workspaceId, topic, watch) {
215
217
  return topic === 'files'
216
218
  ? `${workspaceId}:files:${JSON.stringify(watch.topics.files)}`
217
- : `${workspaceId}:${topic}`;
219
+ : `${workspaceId}:${topic}:${watch.topics.sessionGroup ?? 'ALL'}`;
218
220
  }
219
221
  readActive(workspaceId, topic) {
220
222
  const prefix = `${workspaceId}:${topic}`;
package/dist/terminal.js CHANGED
@@ -631,7 +631,7 @@ function terminalDimension(value, fallback) {
631
631
  function terminalSequence(value) {
632
632
  if (value === undefined)
633
633
  return 0;
634
- if (typeof value !== 'number' || !Number.isInteger(value) || value < 0)
634
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
635
635
  throw new Error('TERMINAL_REPLAY_INVALID');
636
636
  return value;
637
637
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.11.18",
3
+ "version": "0.11.20",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -37,10 +37,10 @@
37
37
  "playwright": "1.62.1",
38
38
  "ws": "^8.21.3",
39
39
  "zod": "4.4.3",
40
- "@myagentroam/agent": "0.11.18",
41
- "@myagentroam/marctl": "0.11.0",
40
+ "@myagentroam/agent": "0.11.20",
41
+ "@myagentroam/protocol": "0.11.20",
42
42
  "@myagentroam/code-navigation": "0.11.0",
43
- "@myagentroam/protocol": "0.11.18"
43
+ "@myagentroam/marctl": "0.11.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/ws": "^8.18.1"