@myagentroam/node 0.11.30 → 0.11.31

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.
package/dist/connector.js CHANGED
@@ -131,6 +131,7 @@ const HOSTED_CONVERSATION_OPERATIONS = [
131
131
  'conversation.hosted.rollback',
132
132
  'conversation.hosted.interrupt',
133
133
  'conversation.hosted.config.update',
134
+ 'conversation.hosted.template.switch',
134
135
  'conversation.hosted.metadata.update',
135
136
  'conversation.hosted.image.read',
136
137
  'conversation.hosted.tool-resource.read',
@@ -6,7 +6,7 @@ import { randomUUID } from 'node:crypto';
6
6
  import { constants } from 'node:fs';
7
7
  import { lstat, mkdir, open, readFile, rename, rm, utimes } from 'node:fs/promises';
8
8
  import { join } from 'node:path';
9
- 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';
9
+ import { toolResultResourceInputSchema, toolResultResourcePageSchema, conversationTurnSchema, conversationTemplateSummarySchema, hostedConversationConfigUpdateInputSchema, hostedConversationTemplateSwitchInputSchema, hostedConversationTemplateSwitchResultSchema, 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';
10
10
  import { hostedConversationGeneratedImagesDisplayDirectory, hostedConversationImageRunId, marAgentGeneratedImagesDirectory } from '../runner/mar-agent/generated-images.js';
11
11
  import { sessionTitleFromFirstMessage } from '../util/node-operation-parsers.js';
12
12
  import { safeErrorCode } from '../util/safe-error.js';
@@ -62,6 +62,7 @@ export class HostedConversationNodeService {
62
62
  'conversation.hosted.rollback': (raw) => this.rollback(raw),
63
63
  'conversation.hosted.interrupt': (raw) => this.interrupt(raw),
64
64
  'conversation.hosted.config.update': (raw) => this.updateConfiguration(raw),
65
+ 'conversation.hosted.template.switch': (raw) => this.switchTemplate(raw),
65
66
  'conversation.hosted.metadata.update': (raw) => this.updateMetadata(raw),
66
67
  'conversation.hosted.image.read': (raw) => this.readImage(raw),
67
68
  'conversation.hosted.tool-resource.read': (raw) => this.readToolResource(raw),
@@ -147,8 +148,11 @@ export class HostedConversationNodeService {
147
148
  if (pending !== undefined)
148
149
  return pending;
149
150
  const existing = this.options.store.findConversationByClientRequest(payload.userId, input.clientRequestId);
150
- if (existing !== undefined)
151
+ if (existing !== undefined) {
152
+ if (existing.templateId !== input.templateId)
153
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
151
154
  return this.snapshot(payload.userId, existing.id, models);
155
+ }
152
156
  const operation = this.createConversation(payload.userId, input.clientRequestId, template, models, input.modelId, payload.runtimeScopeId);
153
157
  this.pendingCreates.set(key, operation);
154
158
  void operation.finally(() => this.pendingCreates.delete(key)).catch(() => undefined);
@@ -233,7 +237,8 @@ export class HostedConversationNodeService {
233
237
  async get(raw) {
234
238
  const payload = hostedRequest(raw);
235
239
  const input = hostedConversationIdInputSchema.parse(payload.data);
236
- return this.snapshot(payload.userId, input.conversationId, runtimeModels(payload.models), payload.templateDeleted);
240
+ this.assertTemplateContext(payload, this.options.store.requireConversation(payload.userId, input.conversationId));
241
+ return this.snapshot(payload.userId, input.conversationId, runtimeModels(payload.models), payload.templateDeleted, false, undefined, payload.sourceTemplateId);
237
242
  }
238
243
  async history(raw) {
239
244
  const payload = hostedRequest(raw);
@@ -250,9 +255,9 @@ export class HostedConversationNodeService {
250
255
  });
251
256
  return hostedConversationHistoryPageSchema.parse(page);
252
257
  }
253
- async snapshot(userId, conversationId, availableModels, templateDeleted = false, includeActiveHistoryMutation = false, historyOverride) {
258
+ async snapshot(userId, conversationId, availableModels, templateDeleted = false, includeActiveHistoryMutation = false, historyOverride, sourceTemplateId) {
254
259
  if (includeActiveHistoryMutation)
255
- return this.readSnapshot(userId, conversationId, availableModels, templateDeleted, historyOverride);
260
+ return this.readSnapshot(userId, conversationId, availableModels, templateDeleted, historyOverride, sourceTemplateId);
256
261
  while (true) {
257
262
  const version = this.historyVersions.get(conversationId) ?? 0;
258
263
  const mutation = this.historyMutations.get(conversationId);
@@ -260,16 +265,20 @@ export class HostedConversationNodeService {
260
265
  await mutation.catch(() => undefined);
261
266
  continue;
262
267
  }
263
- const snapshot = await this.readSnapshot(userId, conversationId, availableModels, templateDeleted);
268
+ const snapshot = await this.readSnapshot(userId, conversationId, availableModels, templateDeleted, undefined, sourceTemplateId);
264
269
  if (!this.historyMutations.has(conversationId) &&
265
270
  (this.historyVersions.get(conversationId) ?? 0) === version)
266
271
  return snapshot;
267
272
  }
268
273
  }
269
- async readSnapshot(userId, conversationId, availableModels, templateDeleted, historyOverride) {
274
+ async readSnapshot(userId, conversationId, availableModels, templateDeleted, historyOverride, sourceTemplateId) {
270
275
  let conversation = this.options.store.requireConversation(userId, conversationId);
276
+ if (sourceTemplateId !== undefined && conversation.templateId !== sourceTemplateId)
277
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
278
+ const templateId = conversation.templateId;
279
+ const activeVersionId = conversation.activeVersionId;
271
280
  if (templateDeleted)
272
- conversation = await this.options.store.retainConversationTemplate(userId, conversationId, this.now());
281
+ conversation = await this.options.store.retainConversationTemplate(userId, conversationId, this.now(), sourceTemplateId ?? templateId);
273
282
  const latestVersionId = conversation.templateDeleted
274
283
  ? undefined
275
284
  : this.latestAvailableTemplateVersionId(conversation.templateId);
@@ -282,7 +291,11 @@ export class HostedConversationNodeService {
282
291
  const configuredModelId = conversation.modelId;
283
292
  const selected = selectModel(version, availableModels, conversation.modelId);
284
293
  if (selected.id !== conversation.modelId)
285
- conversation = await this.options.store.updateConversation(conversation.id, (current) => current.modelId === configuredModelId ? { ...current, modelId: selected.id } : current);
294
+ conversation = await this.options.store.updateConversation(conversation.id, (current) => current.templateId === templateId &&
295
+ current.activeVersionId === activeVersionId &&
296
+ current.modelId === configuredModelId
297
+ ? { ...current, modelId: selected.id }
298
+ : current);
286
299
  }
287
300
  const paths = this.options.store.pathsForConversation(userId, conversation.id);
288
301
  const history = historyOverride ??
@@ -293,6 +306,9 @@ export class HostedConversationNodeService {
293
306
  modelId: conversation.modelId,
294
307
  limit: 10
295
308
  }));
309
+ const current = this.options.store.requireConversation(userId, conversationId);
310
+ if (current.templateId !== templateId || current.activeVersionId !== activeVersionId)
311
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
296
312
  return hostedConversationSnapshotSchema.parse({
297
313
  nodeAppVersion: this.options.nodeAppVersion?.() ?? 'unknown',
298
314
  conversation: this.summary(conversation),
@@ -312,6 +328,7 @@ export class HostedConversationNodeService {
312
328
  const payload = hostedRequest(raw);
313
329
  const input = hostedConversationWatchInputSchema.parse(payload.data);
314
330
  const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
331
+ this.assertTemplateContext(payload, conversation);
315
332
  if (this.cleanupLocks.has(conversation.id))
316
333
  throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
317
334
  const sessionHash = requireWatchSessionHash(payload.sessionHash);
@@ -479,6 +496,7 @@ export class HostedConversationNodeService {
479
496
  return Promise.reject(new Error('CONVERSATION_HOSTING_UNAVAILABLE'));
480
497
  const payload = hostedRequest(raw);
481
498
  const input = hostedConversationMessageInputSchema.parse(payload.data);
499
+ this.assertTemplateContext(payload, this.options.store.requireConversation(payload.userId, input.conversationId));
482
500
  if (this.historyMutations.has(input.conversationId))
483
501
  return Promise.reject(new Error('HOSTED_CONVERSATION_STATE_INVALID'));
484
502
  const key = `${input.conversationId}\0${input.clientMessageId}`;
@@ -561,6 +579,7 @@ export class HostedConversationNodeService {
561
579
  }
562
580
  async startMessage(payload, input, steerPrompt) {
563
581
  let conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
582
+ this.assertTemplateContext(payload, conversation);
564
583
  if (this.cleanupLocks.has(conversation.id))
565
584
  throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
566
585
  if (this.historyMutations.has(conversation.id))
@@ -770,6 +789,7 @@ export class HostedConversationNodeService {
770
789
  const payload = hostedRequest(raw);
771
790
  const input = hostedConversationRollbackInputSchema.parse(payload.data);
772
791
  const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
792
+ this.assertTemplateContext(payload, conversation);
773
793
  if (conversation.status !== 'ACTIVE' ||
774
794
  this.cleanupLocks.has(conversation.id) ||
775
795
  this.historyMutations.has(conversation.id))
@@ -942,10 +962,94 @@ export class HostedConversationNodeService {
942
962
  throw error;
943
963
  }
944
964
  }
945
- async updateConfiguration(raw) {
965
+ switchTemplate(raw) {
966
+ if (this.closing || this.disablePrepared)
967
+ throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
968
+ const payload = hostedRequest(raw);
969
+ const input = hostedConversationTemplateSwitchInputSchema.parse(payload.data);
970
+ const conversation = this.requireWritableConversation(payload.userId, input.conversationId);
971
+ if (this.fileWrites.has(conversation.id) ||
972
+ [...this.uploads.values()].some((upload) => upload.conversationId === conversation.id && upload.status === 'UPLOADING'))
973
+ throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
974
+ if (this.activeRuns.has(conversation.id) ||
975
+ this.runSettlements.has(conversation.id) ||
976
+ [...this.pendingMessages.keys()].some((key) => key.startsWith(`${conversation.id}\0`)))
977
+ throw new Error('HOSTED_CONVERSATION_RUN_ACTIVE');
978
+ const template = conversationTemplateSummarySchema.parse(payload.template);
979
+ if (template.id !== input.templateId ||
980
+ template.nodeId !== conversation.nodeId ||
981
+ template.status !== 'ACTIVE' ||
982
+ !template.available ||
983
+ template.currentVersionId === null)
984
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_UNAVAILABLE');
985
+ return this.withConversationChange(conversation.id, async () => {
986
+ const current = this.options.store.requireConversation(payload.userId, conversation.id);
987
+ const unchanged = current.templateId === input.templateId;
988
+ if (!unchanged && current.templateId !== input.expectedTemplateId)
989
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
990
+ const version = await this.loadTemplateVersion(unchanged ? current.activeVersionId : template.currentVersionId, template.id);
991
+ const models = networkModels(allowedModels(version, runtimeModels(payload.models)), version.draft.networkMode).filter((model) => model.conversationModel !== false);
992
+ let saved = current;
993
+ if (!unchanged) {
994
+ const selected = selectModel(version, models, current.modelId);
995
+ resolveMcpServers(version, this.options.secretEnvironment ?? process.env);
996
+ const paths = this.options.store.pathsForConversation(payload.userId, current.id);
997
+ this.pathMounts(paths, version);
998
+ this.processExecutor(paths, version);
999
+ if (current.quota.usedBytes > version.draft.quotas.maxStorageBytes)
1000
+ throw new Error('HOSTED_CONVERSATION_QUOTA_EXCEEDED');
1001
+ const updatedAt = this.now();
1002
+ saved = await this.options.store.updateConversation(current.id, (latest) => ({
1003
+ ...latest,
1004
+ templateId: template.id,
1005
+ templateName: template.name,
1006
+ templateDeleted: false,
1007
+ activeVersionId: version.id,
1008
+ modelId: selected.id,
1009
+ quota: { ...latest.quota, maxBytes: version.draft.quotas.maxStorageBytes },
1010
+ updatedAt,
1011
+ lastActivityAt: updatedAt
1012
+ }));
1013
+ try {
1014
+ this.publishHostedEvent(saved.id, { conversationId: saved.id, kind: 'template-changed' });
1015
+ }
1016
+ catch {
1017
+ // Committed configuration does not depend on best-effort Watch delivery.
1018
+ }
1019
+ finally {
1020
+ for (const [key, lease] of this.watchLeases)
1021
+ if (lease.conversationId === saved.id)
1022
+ this.removeWatchLease(key);
1023
+ }
1024
+ }
1025
+ return hostedConversationTemplateSwitchResultSchema.parse({
1026
+ conversation: this.summary(saved),
1027
+ template: templateSummary(saved, version.draft),
1028
+ models: models.map(modelSummary)
1029
+ });
1030
+ });
1031
+ }
1032
+ withConversationChange(conversationId, change) {
1033
+ if (this.historyMutations.has(conversationId))
1034
+ throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
1035
+ const operation = Promise.resolve().then(change);
1036
+ this.historyMutations.set(conversationId, operation);
1037
+ void operation
1038
+ .finally(() => {
1039
+ if (this.historyMutations.get(conversationId) === operation)
1040
+ this.historyMutations.delete(conversationId);
1041
+ })
1042
+ .catch(() => undefined);
1043
+ return operation;
1044
+ }
1045
+ updateConfiguration(raw) {
946
1046
  const payload = hostedRequest(raw);
947
1047
  const input = hostedConversationConfigUpdateInputSchema.parse(payload.data);
948
- const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
1048
+ const conversation = this.requireWritableConversation(payload.userId, input.conversationId);
1049
+ this.assertTemplateContext(payload, conversation);
1050
+ return this.withConversationChange(conversation.id, () => this.saveConfiguration(payload, input, conversation));
1051
+ }
1052
+ async saveConfiguration(payload, input, conversation) {
949
1053
  if (this.cleanupLocks.has(conversation.id))
950
1054
  throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
951
1055
  const latestVersionId = conversation.templateDeleted
@@ -1337,6 +1441,7 @@ export class HostedConversationNodeService {
1337
1441
  const snapshot = await this.get({
1338
1442
  conversationId: lease.conversationId,
1339
1443
  __requestUserId: lease.userId,
1444
+ __sourceTemplateId: lease.templateId,
1340
1445
  __marAgentModels: lease.models
1341
1446
  });
1342
1447
  if (this.watchLeases.get(lease.ownerKey) !== lease)
@@ -1559,12 +1664,17 @@ export class HostedConversationNodeService {
1559
1664
  }
1560
1665
  requireWritableConversation(userId, conversationId) {
1561
1666
  const conversation = this.options.store.requireConversation(userId, conversationId);
1562
- if (this.cleanupLocks.has(conversation.id))
1667
+ if (this.cleanupLocks.has(conversation.id) || this.historyMutations.has(conversation.id))
1563
1668
  throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
1564
1669
  if (conversation.status !== 'ACTIVE')
1565
1670
  throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
1566
1671
  return conversation;
1567
1672
  }
1673
+ assertTemplateContext(payload, conversation) {
1674
+ if (payload.sourceTemplateId !== undefined &&
1675
+ payload.sourceTemplateId !== conversation.templateId)
1676
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
1677
+ }
1568
1678
  requireUpload(userId, conversationId, uploadId) {
1569
1679
  this.options.store.requireConversation(userId, conversationId);
1570
1680
  const upload = this.uploads.get(uploadId);
@@ -1800,18 +1910,22 @@ export class HostedConversationNodeService {
1800
1910
  function hostedRequest(raw) {
1801
1911
  if (!record(raw))
1802
1912
  throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
1803
- const { __usageGrant, __requestUserId, __template, __marAgentModel, __marAgentModels, __marAgentCompact, __marAgentRuntimeScopeId, __workspaceWatchSessionHash, __templateDeleted, ...data } = raw;
1913
+ const { __usageGrant, __requestUserId, __template, __marAgentModel, __marAgentModels, __marAgentCompact, __marAgentRuntimeScopeId, __workspaceWatchSessionHash, __templateDeleted, __sourceTemplateId, ...data } = raw;
1804
1914
  if (typeof __requestUserId !== 'number' ||
1805
1915
  !Number.isSafeInteger(__requestUserId) ||
1806
1916
  __requestUserId <= 0)
1807
1917
  throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
1808
1918
  if (__templateDeleted !== undefined && __templateDeleted !== true)
1809
1919
  throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
1920
+ if (__sourceTemplateId !== undefined &&
1921
+ (typeof __sourceTemplateId !== 'string' || __sourceTemplateId.length === 0))
1922
+ throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
1810
1923
  if (__marAgentRuntimeScopeId !== undefined &&
1811
1924
  (typeof __marAgentRuntimeScopeId !== 'string' || __marAgentRuntimeScopeId.length === 0))
1812
1925
  throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
1813
1926
  return {
1814
1927
  data,
1928
+ ...(__sourceTemplateId === undefined ? {} : { sourceTemplateId: __sourceTemplateId }),
1815
1929
  ...(__usageGrant === undefined
1816
1930
  ? {}
1817
1931
  : { usageGrant: modelUsageGrantSchema.parse(__usageGrant) }),
@@ -385,10 +385,15 @@ export class HostedConversationStore {
385
385
  await this.writeManifest(paths.manifestPath, manifest);
386
386
  return manifest;
387
387
  }
388
- async retainConversationTemplate(userId, conversationId, updatedAt) {
388
+ async retainConversationTemplate(userId, conversationId, updatedAt, expectedTemplateId) {
389
+ return this.withConversationMutation(conversationId, () => this.retainConversationTemplateUnlocked(userId, conversationId, updatedAt, expectedTemplateId));
390
+ }
391
+ async retainConversationTemplateUnlocked(userId, conversationId, updatedAt, expectedTemplateId) {
389
392
  if (!Number.isSafeInteger(updatedAt) || updatedAt < 0)
390
393
  throw new Error('HOSTED_CONVERSATION_INPUT_INVALID');
391
394
  const current = this.requireConversation(userId, conversationId);
395
+ if (expectedTemplateId !== undefined && current.templateId !== expectedTemplateId)
396
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
392
397
  const version = this.options.database.raw
393
398
  .prepare('SELECT status FROM conversation_template_versions WHERE id=? AND template_id=?')
394
399
  .get(current.activeVersionId, current.templateId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.11.30",
3
+ "version": "0.11.31",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -38,10 +38,10 @@
38
38
  "sharp": "0.35.5",
39
39
  "ws": "^8.21.3",
40
40
  "zod": "4.4.3",
41
- "@myagentroam/agent": "0.11.30",
41
+ "@myagentroam/agent": "0.11.31",
42
42
  "@myagentroam/code-navigation": "0.11.0",
43
43
  "@myagentroam/marctl": "0.11.1",
44
- "@myagentroam/protocol": "0.11.30"
44
+ "@myagentroam/protocol": "0.11.31"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/ws": "^8.18.1"