@myagentroam/node 0.9.119 → 0.9.121

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.
@@ -24,13 +24,11 @@ export class CodexRunner extends AbstractRunner {
24
24
  fastEnabled = false;
25
25
  environmentFingerprint;
26
26
  environmentRuns = new Set();
27
- profileCache = new Map();
28
- profileCacheTimers = new Map();
29
- profileRefreshes = new Map();
27
+ profileCache;
28
+ profileCacheVersion;
29
+ profileCacheTimer;
30
+ profileRefresh;
30
31
  profileClients = new Set();
31
- profileKeyByEnvironment = new Map();
32
- latestProfileCache;
33
- selectedProfileKey;
34
32
  profileGeneration = 0;
35
33
  constructor(client = new CodexAppServerClient(), profileClientFactory = () => new CodexAppServerClient()) {
36
34
  super();
@@ -43,40 +41,39 @@ export class CodexRunner extends AbstractRunner {
43
41
  claudeCodeAvailable: capabilities.claudeCode.available,
44
42
  openCodeAvailable: capabilities.openCode?.available === true
45
43
  }).find((profile) => profile.runner === this.name);
46
- return { ...base, models: this.latestProfileCache?.profile.models ?? [] };
44
+ return { ...base, models: this.profileCache?.profile.models ?? [] };
47
45
  }
48
46
  cachedProfile(capabilities, environment = {}) {
49
47
  const base = this.profile(capabilities);
50
- const key = codexEnvironmentFingerprint(environment, capabilities.codex.version);
51
- const cached = this.profileCache.get(key);
52
- return cached === undefined ? { ...base, models: [] } : cached.profile;
48
+ return this.profileCache === undefined ||
49
+ this.profileCacheVersion !== capabilities.codex.version
50
+ ? { ...base, models: [] }
51
+ : this.profileCache.profile;
53
52
  }
54
53
  async refreshProfile(capabilities, workspace, environment = {}, waitForRefresh = false) {
55
54
  void workspace;
56
55
  const base = this.profile(capabilities);
57
56
  if (!capabilities.codex.available)
58
57
  return { ...base, models: [] };
59
- const key = codexEnvironmentFingerprint(environment, capabilities.codex.version);
60
- this.profileKeyByEnvironment.set(codexSecretEnvironmentFingerprint(environment), key);
61
- this.selectedProfileKey = key;
58
+ const version = capabilities.codex.version;
62
59
  const now = Date.now();
63
- const cached = this.profileCache.get(key);
60
+ const cached = this.profileCacheVersion === version ? this.profileCache : undefined;
64
61
  if (cached?.successful === true && now < cached.fetchedAt + CODEX_MODEL_CACHE_MS)
65
- return this.selectCachedProfile(key, cached);
62
+ return cached.profile;
66
63
  if (cached?.successful === true) {
67
64
  if (now >= cached.retryAt) {
68
- const refresh = this.refreshCodexProfile(key, base, environment);
65
+ const refresh = this.refreshCodexProfile(version, base, environment);
69
66
  if (waitForRefresh)
70
67
  return refresh;
71
68
  }
72
- return this.selectCachedProfile(key, cached);
69
+ return cached.profile;
73
70
  }
74
71
  if (cached !== undefined && now < cached.retryAt)
75
72
  return cached.profile;
76
- return this.refreshCodexProfile(key, base, environment);
73
+ return this.refreshCodexProfile(version, base, environment);
77
74
  }
78
- refreshCodexProfile(key, base, environment) {
79
- const existing = this.profileRefreshes.get(key);
75
+ refreshCodexProfile(version, base, environment) {
76
+ const existing = this.profileRefresh;
80
77
  if (existing !== undefined)
81
78
  return existing;
82
79
  const generation = this.profileGeneration;
@@ -88,11 +85,8 @@ export class CodexRunner extends AbstractRunner {
88
85
  successful: true,
89
86
  retryAt: 0
90
87
  };
91
- if (generation === this.profileGeneration) {
92
- this.cacheProfile(key, cache);
93
- if (this.selectedProfileKey === key)
94
- this.latestProfileCache = cache;
95
- }
88
+ if (generation === this.profileGeneration)
89
+ this.cacheProfile(version, cache);
96
90
  return cache.profile;
97
91
  })
98
92
  .catch((error) => {
@@ -101,12 +95,12 @@ export class CodexRunner extends AbstractRunner {
101
95
  });
102
96
  if (generation !== this.profileGeneration)
103
97
  return { ...base, models: [] };
104
- const cached = this.profileCache.get(key);
98
+ const cached = this.profileCacheVersion === version ? this.profileCache : undefined;
105
99
  if (cached !== undefined) {
106
100
  cached.retryAt = Date.now() + CODEX_MODEL_FAILURE_RETRY_MS;
107
101
  return cached.profile;
108
102
  }
109
- this.cacheProfile(key, {
103
+ this.cacheProfile(version, {
110
104
  profile: { ...base, models: [] },
111
105
  defaultConfiguration: emptyCodexConfiguration(),
112
106
  fetchedAt: 0,
@@ -115,33 +109,24 @@ export class CodexRunner extends AbstractRunner {
115
109
  });
116
110
  return { ...base, models: [] };
117
111
  })
118
- .finally(() => this.profileRefreshes.delete(key));
119
- this.profileRefreshes.set(key, refresh);
112
+ .finally(() => {
113
+ if (this.profileRefresh === refresh)
114
+ this.profileRefresh = undefined;
115
+ });
116
+ this.profileRefresh = refresh;
120
117
  return refresh;
121
118
  }
122
- selectCachedProfile(key, cached) {
123
- if (cached.successful && this.selectedProfileKey === key)
124
- this.latestProfileCache = cached;
125
- return cached.profile;
126
- }
127
- cacheProfile(key, cache) {
128
- const previousTimer = this.profileCacheTimers.get(key);
129
- if (previousTimer !== undefined)
130
- clearTimeout(previousTimer);
131
- this.profileCache.set(key, cache);
132
- const timer = setTimeout(() => {
133
- this.profileCacheTimers.delete(key);
134
- this.profileCache.delete(key);
135
- for (const [environmentKey, profileKey] of this.profileKeyByEnvironment)
136
- if (profileKey === key)
137
- this.profileKeyByEnvironment.delete(environmentKey);
138
- if (this.selectedProfileKey === key) {
139
- this.selectedProfileKey = undefined;
140
- this.latestProfileCache = undefined;
141
- }
119
+ cacheProfile(version, cache) {
120
+ if (this.profileCacheTimer !== undefined)
121
+ clearTimeout(this.profileCacheTimer);
122
+ this.profileCache = cache;
123
+ this.profileCacheVersion = version;
124
+ this.profileCacheTimer = setTimeout(() => {
125
+ this.profileCacheTimer = undefined;
126
+ this.profileCache = undefined;
127
+ this.profileCacheVersion = undefined;
142
128
  }, CODEX_PROFILE_ENTRY_TTL_MS);
143
- timer.unref();
144
- this.profileCacheTimers.set(key, timer);
129
+ this.profileCacheTimer.unref();
145
130
  }
146
131
  async fetchCodexProfile(base, environment) {
147
132
  const client = this.profileClientFactory();
@@ -306,11 +291,8 @@ export class CodexRunner extends AbstractRunner {
306
291
  profile.models.some((model) => model.id === configuration.model && model.supportedEfforts.includes(configuration.effort)) &&
307
292
  profile.accessOptions.some((option) => option.id === configuration.access));
308
293
  }
309
- profileCacheForEnvironment(environment) {
310
- if (environment === undefined)
311
- return this.latestProfileCache;
312
- const key = this.profileKeyByEnvironment.get(codexSecretEnvironmentFingerprint(environment));
313
- return key === undefined ? undefined : this.profileCache.get(key);
294
+ profileCacheForEnvironment(_environment) {
295
+ return this.profileCache;
314
296
  }
315
297
  async resumeExternal(external, context) {
316
298
  if (external.runner !== this.name ||
@@ -483,13 +465,12 @@ export class CodexRunner extends AbstractRunner {
483
465
  this.client.clearEnvironment();
484
466
  this.environmentFingerprint = undefined;
485
467
  this.environmentRuns.clear();
486
- for (const timer of this.profileCacheTimers.values())
487
- clearTimeout(timer);
488
- this.profileCacheTimers.clear();
489
- this.profileCache.clear();
490
- this.profileKeyByEnvironment.clear();
491
- this.latestProfileCache = undefined;
492
- this.selectedProfileKey = undefined;
468
+ if (this.profileCacheTimer !== undefined)
469
+ clearTimeout(this.profileCacheTimer);
470
+ this.profileCacheTimer = undefined;
471
+ this.profileCache = undefined;
472
+ this.profileCacheVersion = undefined;
473
+ this.profileRefresh = undefined;
493
474
  }
494
475
  request(method, params) {
495
476
  return this.client.request(method, params);
@@ -537,19 +518,6 @@ export class CodexRunner extends AbstractRunner {
537
518
  function emptyCodexConfiguration() {
538
519
  return { runner: 'codex', model: '', effort: '', access: 'on-request' };
539
520
  }
540
- function codexEnvironmentFingerprint(environment, version) {
541
- return createHash('sha256')
542
- .update(JSON.stringify({
543
- environment: Object.entries(environment).sort(([left], [right]) => left.localeCompare(right)),
544
- version: version ?? null
545
- }))
546
- .digest('base64url');
547
- }
548
- function codexSecretEnvironmentFingerprint(environment) {
549
- return createHash('sha256')
550
- .update(JSON.stringify(Object.entries(environment).sort(([left], [right]) => left.localeCompare(right))))
551
- .digest('base64url');
552
- }
553
521
  function codexModelPage(value) {
554
522
  if (!isPlainRecord(value) || !Array.isArray(value.data))
555
523
  throw new Error('CODEX_MODELS_INVALID');
@@ -244,6 +244,7 @@ export class MarAgentManagedRunController {
244
244
  let executionModels = [];
245
245
  try {
246
246
  const sessionKey = input.externalSessionId ?? input.sessionId;
247
+ // Reserve both identities while opening so a concurrent release can await the same runtime.
247
248
  const acquisitionKeys = [...new Set([sessionKey, input.sessionId])];
248
249
  if (acquisitionKeys.some((key) => this.#runtimeAcquisitions.get(key) !== undefined))
249
250
  throw new Error('MAR_AGENT_SESSION_RUNTIME_ACTIVE');
@@ -18,9 +18,9 @@ export class OpenCodeRunner extends AbstractRunner {
18
18
  discoveryScope = 'WORKSPACE_REQUIRED';
19
19
  contextLimits = new Map();
20
20
  contextLimitsExpiresAt = 0;
21
- profileCache = new Map();
22
- profileRetryAt = new Map();
23
- profileRefreshes = new Map();
21
+ profileCache;
22
+ profileRetryAt = 0;
23
+ profileRefresh;
24
24
  profileGeneration = 0;
25
25
  execution = new Map();
26
26
  managedTurns = new Map();
@@ -41,7 +41,7 @@ export class OpenCodeRunner extends AbstractRunner {
41
41
  }
42
42
  cachedProfile(capabilities, environment = {}) {
43
43
  this.pruneProfileCaches(Date.now());
44
- const cached = this.profileCache.get(this.environmentFingerprint(environment));
44
+ const cached = this.profileCache;
45
45
  return cached === undefined
46
46
  ? this.profile(capabilities)
47
47
  : { ...this.profile(capabilities), models: cached.models };
@@ -50,10 +50,9 @@ export class OpenCodeRunner extends AbstractRunner {
50
50
  if (capabilities.openCode?.available !== true) {
51
51
  return this.profile(capabilities);
52
52
  }
53
- const cacheKey = this.environmentFingerprint(environment);
54
53
  const now = Date.now();
55
54
  this.pruneProfileCaches(now);
56
- const cached = this.profileCache.get(cacheKey);
55
+ const cached = this.profileCache;
57
56
  if (cached !== undefined && cached.expiresAt > now) {
58
57
  nodeLog('runner.opencode.profile.cache-hit', {
59
58
  models: cached.models.length,
@@ -63,24 +62,24 @@ export class OpenCodeRunner extends AbstractRunner {
63
62
  this.contextLimitsExpiresAt = cached.expiresAt;
64
63
  return { ...this.profile(capabilities), models: cached.models };
65
64
  }
66
- if (now < (this.profileRetryAt.get(cacheKey) ?? 0))
65
+ if (now < this.profileRetryAt)
67
66
  return cached === undefined
68
67
  ? this.profile(capabilities)
69
68
  : { ...this.profile(capabilities), models: cached.models };
70
- const current = this.profileRefreshes.get(cacheKey);
69
+ const current = this.profileRefresh;
71
70
  if (current !== undefined) {
72
71
  nodeLog('runner.opencode.profile.single-flight-joined', {});
73
72
  return current;
74
73
  }
75
74
  const generation = this.profileGeneration;
76
- const refresh = this.fetchProfile(capabilities, environment, cacheKey, generation, cached).finally(() => {
77
- if (this.profileRefreshes.get(cacheKey) === refresh)
78
- this.profileRefreshes.delete(cacheKey);
75
+ const refresh = this.fetchProfile(capabilities, environment, generation, cached).finally(() => {
76
+ if (this.profileRefresh === refresh)
77
+ this.profileRefresh = undefined;
79
78
  });
80
- this.profileRefreshes.set(cacheKey, refresh);
79
+ this.profileRefresh = refresh;
81
80
  return refresh;
82
81
  }
83
- async fetchProfile(capabilities, environment, cacheKey, generation, stale) {
82
+ async fetchProfile(capabilities, environment, generation, stale) {
84
83
  const client = this.managedClient(environment, 'runner-profile');
85
84
  const startedAt = performance.now();
86
85
  try {
@@ -92,14 +91,13 @@ export class OpenCodeRunner extends AbstractRunner {
92
91
  return this.profile(capabilities);
93
92
  this.contextLimits = contextLimits;
94
93
  this.contextLimitsExpiresAt = expiresAt;
95
- this.profileCache.set(cacheKey, {
94
+ this.profileCache = {
96
95
  expiresAt,
97
96
  discardAt: Date.now() + PROFILE_STALE_TTL_MS,
98
- environmentFingerprint: cacheKey,
99
97
  models,
100
98
  contextLimits
101
- });
102
- this.profileRetryAt.delete(cacheKey);
99
+ };
100
+ this.profileRetryAt = 0;
103
101
  nodeLog('runner.opencode.profile.providers.completed', {
104
102
  models: models.length,
105
103
  durationMs: Math.round(performance.now() - startedAt)
@@ -109,7 +107,7 @@ export class OpenCodeRunner extends AbstractRunner {
109
107
  catch {
110
108
  if (generation !== this.profileGeneration)
111
109
  return this.profile(capabilities);
112
- this.profileRetryAt.set(cacheKey, Date.now() + PROFILE_FAILURE_RETRY_MS);
110
+ this.profileRetryAt = Date.now() + PROFILE_FAILURE_RETRY_MS;
113
111
  nodeLog('runner.opencode.profile.providers.failed', {
114
112
  durationMs: Math.round(performance.now() - startedAt)
115
113
  });
@@ -121,10 +119,6 @@ export class OpenCodeRunner extends AbstractRunner {
121
119
  ? this.profile(capabilities)
122
120
  : { ...this.profile(capabilities), models: stale.models };
123
121
  }
124
- environmentFingerprint(environment) {
125
- const entries = Object.entries(environment).sort(([left], [right]) => left.localeCompare(right));
126
- return createHash('sha256').update(JSON.stringify(entries)).digest('hex');
127
- }
128
122
  available(capabilities) {
129
123
  return capabilities.openCode?.available === true;
130
124
  }
@@ -160,21 +154,16 @@ export class OpenCodeRunner extends AbstractRunner {
160
154
  }
161
155
  cachedModels(environment) {
162
156
  this.pruneProfileCaches(Date.now());
163
- const fingerprint = this.environmentFingerprint(environment);
164
- return [...this.profileCache.values()]
165
- .filter((entry) => entry.environmentFingerprint === fingerprint)
166
- .flatMap((entry) => entry.models);
157
+ void environment;
158
+ return this.profileCache?.models ?? [];
167
159
  }
168
160
  pruneProfileCaches(now) {
169
- for (const [key, entry] of this.profileCache) {
170
- if (entry.discardAt > now)
171
- continue;
172
- this.profileCache.delete(key);
173
- this.profileRetryAt.delete(key);
174
- }
175
- for (const [key, retryAt] of this.profileRetryAt)
176
- if (retryAt <= now && !this.profileCache.has(key) && !this.profileRefreshes.has(key))
177
- this.profileRetryAt.delete(key);
161
+ if (this.profileCache !== undefined && this.profileCache.discardAt <= now)
162
+ this.profileCache = undefined;
163
+ if (this.profileRetryAt <= now &&
164
+ this.profileCache === undefined &&
165
+ this.profileRefresh === undefined)
166
+ this.profileRetryAt = 0;
178
167
  }
179
168
  mcpConfiguration(raw, secrets) {
180
169
  const servers = super.mcpConfiguration(raw, secrets);
@@ -340,9 +329,9 @@ export class OpenCodeRunner extends AbstractRunner {
340
329
  execution.abort.abort();
341
330
  this.execution.clear();
342
331
  this.managedTurns.clear();
343
- this.profileCache.clear();
344
- this.profileRefreshes.clear();
345
- this.profileRetryAt.clear();
332
+ this.profileCache = undefined;
333
+ this.profileRefresh = undefined;
334
+ this.profileRetryAt = 0;
346
335
  this.contextLimits = new Map();
347
336
  this.contextLimitsExpiresAt = 0;
348
337
  const stopping = [
@@ -41,6 +41,14 @@ export class RunnerRegistry {
41
41
  try {
42
42
  return await runner.refreshProfile(capabilities, workspace, environment, waitForRefresh);
43
43
  }
44
+ catch (error) {
45
+ nodeLog('runner.profile.refresh.failed', {
46
+ runner: runner.name,
47
+ workspaceId: workspace?.id,
48
+ error: error instanceof Error ? error.message : String(error)
49
+ });
50
+ return runner.cachedProfile(capabilities, environment);
51
+ }
44
52
  finally {
45
53
  const durationMs = Math.round(performance.now() - startedAt);
46
54
  if (durationMs >= 100)
@@ -515,8 +515,7 @@ export class ConversationTemplateNodeService {
515
515
  manifest.entries.length !== numberValue(row, 'file_count') ||
516
516
  manifest.entries.reduce((total, entry) => total + entry.size, 0) !==
517
517
  numberValue(row, 'total_bytes') ||
518
- snapshotContentHash(manifest.directories, manifest.entries, manifest.schemaVersion) !==
519
- manifest.contentHash)
518
+ snapshotContentHash(manifest.directories, manifest.entries) !== manifest.contentHash)
520
519
  throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
521
520
  const expected = new Map(manifest.entries.map((entry) => [entry.relativePath, entry]));
522
521
  const expectedDirectories = new Set([...manifest.directories, 'inbox', 'output']);
@@ -734,17 +733,6 @@ async function listSnapshotEntries(root, expectedDirectories, actualDirectories,
734
733
  }
735
734
  return entries;
736
735
  }
737
- function legacyPublishedDirectories(entries) {
738
- const directories = new Set(['inbox', 'output']);
739
- for (const entry of entries) {
740
- const parts = entry.relativePath.split('/');
741
- for (let index = 1; index < parts.length; index += 1)
742
- directories.add(parts.slice(0, index).join('/'));
743
- }
744
- directories.delete('inbox');
745
- directories.delete('output');
746
- return [...directories].sort((left, right) => left.localeCompare(right, 'en-US'));
747
- }
748
736
  function forcedExcluded(relativePath) {
749
737
  const parts = relativePath.split('/');
750
738
  if (parts.some((part) => {
@@ -814,19 +802,6 @@ function templateVersionManifest(input) {
814
802
  typeof input['mcpConfigHash'] !== 'string' ||
815
803
  typeof input['contentHash'] !== 'string')
816
804
  throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
817
- if (input['schemaVersion'] === 1) {
818
- if (!Array.isArray(input['files']))
819
- throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
820
- const entries = input['files'].map((value) => manifestFile(value));
821
- return {
822
- schemaVersion: 1,
823
- directories: legacyPublishedDirectories(entries),
824
- entries,
825
- contentHash: input['contentHash'],
826
- mcpServers,
827
- mcpConfigHash: input['mcpConfigHash']
828
- };
829
- }
830
805
  if (input['schemaVersion'] !== 2 ||
831
806
  typeof input['sourceWorkspacePath'] !== 'string' ||
832
807
  !Array.isArray(input['directories']) ||
@@ -913,15 +888,7 @@ function safeNonNegativeInteger(input) {
913
888
  throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
914
889
  return input;
915
890
  }
916
- function snapshotContentHash(directories, entries, schemaVersion = 2) {
917
- if (schemaVersion === 1)
918
- return hashText(entries
919
- .map((entry) => {
920
- if (entry.kind !== 'FILE')
921
- throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
922
- return `${entry.relativePath}\0${entry.size}\0${entry.sha256}`;
923
- })
924
- .join('\n'));
891
+ function snapshotContentHash(directories, entries) {
925
892
  return hashText([
926
893
  ...directories.map((directory) => `DIRECTORY\0${directory}`),
927
894
  ...entries.map((entry) => entry.kind === 'FILE'
@@ -9,6 +9,7 @@ export class GoalMcpServer {
9
9
  goals;
10
10
  changed;
11
11
  capabilities = new Map();
12
+ sockets = new Set();
12
13
  endpoint = process.platform === 'win32'
13
14
  ? `\\\\.\\pipe\\mar-goal-${process.pid}-${randomUUID()}`
14
15
  : join(tmpdir(), `mar-goal-${process.pid}-${randomUUID()}.sock`);
@@ -48,7 +49,10 @@ export class GoalMcpServer {
48
49
  return;
49
50
  const server = await this.listening;
50
51
  this.capabilities.clear();
51
- await new Promise((resolve, reject) => server.close((error) => (error === undefined ? resolve() : reject(error))));
52
+ const closed = new Promise((resolve, reject) => server.close((error) => (error === undefined ? resolve() : reject(error))));
53
+ for (const socket of this.sockets)
54
+ socket.destroy();
55
+ await closed;
52
56
  if (process.platform !== 'win32')
53
57
  await unlink(this.endpoint).catch(() => undefined);
54
58
  }
@@ -65,6 +69,8 @@ export class GoalMcpServer {
65
69
  this.capabilities.set(token, { ...capability, sessionId: nextId });
66
70
  }
67
71
  handle(socket) {
72
+ this.sockets.add(socket);
73
+ socket.once('close', () => this.sockets.delete(socket));
68
74
  let body = '';
69
75
  socket.setEncoding('utf8');
70
76
  socket.on('data', (chunk) => {
@@ -1537,9 +1537,7 @@ export class HostedConversationNodeService {
1537
1537
  processExecutor(paths, version, temporaryDirectory = paths.temporaryRootDirectory, generatedImagesDirectory, generatedImagesLogicalPath) {
1538
1538
  return this.options.processExecutorFactory({
1539
1539
  workspaceDirectory: version.templateDirectory,
1540
- ...(version.sourceWorkspacePath === undefined
1541
- ? {}
1542
- : { sourceWorkspacePath: version.sourceWorkspacePath }),
1540
+ sourceWorkspacePath: version.sourceWorkspacePath,
1543
1541
  inboxDirectory: paths.uploadsDirectory,
1544
1542
  outputDirectory: paths.outputDirectory,
1545
1543
  ...(generatedImagesDirectory === undefined || generatedImagesLogicalPath === undefined
@@ -1605,10 +1603,10 @@ export class HostedConversationNodeService {
1605
1603
  if (!record(persisted))
1606
1604
  throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
1607
1605
  const snapshot = publishedTemplateSnapshot(persisted);
1608
- const parsedDraft = publishedTemplateDraft(snapshot.schemaVersion, persisted.draft);
1606
+ const parsedDraft = publishedTemplateDraft(persisted.draft);
1609
1607
  const draft = parsedDraft.draft;
1610
1608
  const mcpServers = publishedMcpServers(persisted.mcpServers);
1611
- const contentHash = publishedTemplateContentHash(snapshot.directories, snapshot.entries, snapshot.schemaVersion);
1609
+ const contentHash = publishedTemplateContentHash(snapshot.directories, snapshot.entries);
1612
1610
  const totalBytes = snapshot.entries.reduce((total, entry) => total + entry.size, 0);
1613
1611
  if (persisted.mcpConfigHash !== row.mcp_config_hash ||
1614
1612
  createHash('sha256').update(JSON.stringify(mcpServers)).digest('hex') !==
@@ -1629,9 +1627,7 @@ export class HostedConversationNodeService {
1629
1627
  templateId,
1630
1628
  storageKey: row.storage_key,
1631
1629
  templateDirectory: contentDirectory,
1632
- ...(snapshot.sourceWorkspacePath === undefined
1633
- ? {}
1634
- : { sourceWorkspacePath: snapshot.sourceWorkspacePath }),
1630
+ sourceWorkspacePath: snapshot.sourceWorkspacePath,
1635
1631
  draft,
1636
1632
  modelPolicy: parsedDraft.modelPolicy,
1637
1633
  mcpServers
@@ -1679,63 +1675,36 @@ function runtimeModels(value) {
1679
1675
  }
1680
1676
  function allowedModels(version, models) {
1681
1677
  const allowed = new Set(version.modelPolicy.allowedIds);
1682
- return models.filter((model) => model.enabled &&
1683
- allowed.has(version.modelPolicy.identity === 'UPSTREAM_MODEL_ID' ? model.modelId : model.id));
1678
+ return models.filter((model) => model.enabled && allowed.has(model.modelId));
1684
1679
  }
1685
1680
  function selectModel(version, models, requestedModelId) {
1686
1681
  const available = allowedModels(version, models);
1687
1682
  const requested = available.find((candidate) => candidate.id === requestedModelId);
1688
1683
  if (requested !== undefined)
1689
1684
  return requested;
1690
- const defaultModel = available.find((candidate) => (version.modelPolicy.identity === 'UPSTREAM_MODEL_ID' ? candidate.modelId : candidate.id) ===
1691
- version.modelPolicy.defaultId);
1685
+ const defaultModel = available.find((candidate) => candidate.modelId === version.modelPolicy.defaultId);
1692
1686
  if (defaultModel !== undefined)
1693
1687
  return defaultModel;
1694
1688
  const model = version.modelPolicy.allowedIds
1695
- .map((allowedId) => available.find((candidate) => (version.modelPolicy.identity === 'UPSTREAM_MODEL_ID'
1696
- ? candidate.modelId
1697
- : candidate.id) === allowedId))
1689
+ .map((allowedId) => available.find((candidate) => candidate.modelId === allowedId))
1698
1690
  .find((candidate) => candidate !== undefined);
1699
1691
  if (model === undefined)
1700
1692
  throw new Error('HOSTED_CONVERSATION_MODEL_NOT_AVAILABLE');
1701
1693
  return model;
1702
1694
  }
1703
- function publishedTemplateDraft(schemaVersion, value) {
1695
+ function publishedTemplateDraft(value) {
1704
1696
  const current = conversationTemplateDraftSchema.safeParse(value);
1705
1697
  if (current.success) {
1706
1698
  const { allowedModels, defaultModelId, ...draft } = current.data;
1707
1699
  return {
1708
1700
  draft,
1709
1701
  modelPolicy: {
1710
- identity: 'UPSTREAM_MODEL_ID',
1711
1702
  allowedIds: allowedModels.map((model) => model.modelId),
1712
1703
  defaultId: defaultModelId
1713
1704
  }
1714
1705
  };
1715
1706
  }
1716
- if (schemaVersion !== 1 ||
1717
- !record(value) ||
1718
- 'allowedModels' in value ||
1719
- !Array.isArray(value.allowedModelIds))
1720
- throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
1721
- const allowedModelIds = value.allowedModelIds;
1722
- const legacy = { ...value };
1723
- delete legacy.allowedModelIds;
1724
- const normalized = conversationTemplateDraftSchema.safeParse({
1725
- ...legacy,
1726
- allowedModels: allowedModelIds.map((modelId) => ({ modelId, name: modelId }))
1727
- });
1728
- if (!normalized.success)
1729
- throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
1730
- const { allowedModels, defaultModelId, ...draft } = normalized.data;
1731
- return {
1732
- draft,
1733
- modelPolicy: {
1734
- identity: 'CONFIGURATION_ID',
1735
- allowedIds: allowedModels.map((model) => model.modelId),
1736
- defaultId: defaultModelId
1737
- }
1738
- };
1707
+ throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
1739
1708
  }
1740
1709
  function networkModels(models, networkMode) {
1741
1710
  if (networkMode !== 'NONE')
@@ -1773,14 +1742,6 @@ function publishedMcpServers(value) {
1773
1742
  });
1774
1743
  }
1775
1744
  function publishedTemplateSnapshot(manifest) {
1776
- if (manifest.schemaVersion === 1) {
1777
- const entries = publishedTemplateFiles(manifest.files);
1778
- return {
1779
- schemaVersion: 1,
1780
- directories: legacyPublishedTemplateDirectories(entries),
1781
- entries
1782
- };
1783
- }
1784
1745
  if (manifest.schemaVersion !== 2 ||
1785
1746
  typeof manifest.sourceWorkspacePath !== 'string' ||
1786
1747
  !Array.isArray(manifest.directories) ||
@@ -1876,30 +1837,14 @@ function safePublishedTemplateSize(value) {
1876
1837
  throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
1877
1838
  return value;
1878
1839
  }
1879
- function legacyPublishedTemplateDirectories(entries) {
1880
- const directories = new Set();
1881
- for (const entry of entries) {
1882
- const parts = entry.relativePath.split('/');
1883
- for (let index = 1; index < parts.length; index += 1)
1884
- directories.add(parts.slice(0, index).join('/'));
1885
- }
1886
- return [...directories].sort((left, right) => left.localeCompare(right, 'en-US'));
1887
- }
1888
- function publishedTemplateContentHash(directories, entries, schemaVersion) {
1889
- const content = schemaVersion === 1
1890
- ? entries
1891
- .map((entry) => {
1892
- if (entry.kind !== 'FILE')
1893
- throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
1894
- return `${entry.relativePath}\0${entry.size}\0${entry.sha256}`;
1895
- })
1896
- .join('\n')
1897
- : [
1898
- ...directories.map((directory) => `DIRECTORY\0${directory}`),
1899
- ...entries.map((entry) => entry.kind === 'FILE'
1900
- ? `${entry.kind}\0${entry.relativePath}\0${entry.size}\0${entry.executable ? 1 : 0}\0${entry.sha256}`
1901
- : `${entry.kind}\0${entry.relativePath}\0${entry.size}\0${entry.linkTarget}\0${entry.targetKind}\0${entry.resolvedTarget}`)
1902
- ].join('\n');
1840
+ /** Current manifests hash directory and entry identities; changing the format requires a new version. */
1841
+ function publishedTemplateContentHash(directories, entries) {
1842
+ const content = [
1843
+ ...directories.map((directory) => `DIRECTORY\0${directory}`),
1844
+ ...entries.map((entry) => entry.kind === 'FILE'
1845
+ ? `${entry.kind}\0${entry.relativePath}\0${entry.size}\0${entry.executable ? 1 : 0}\0${entry.sha256}`
1846
+ : `${entry.kind}\0${entry.relativePath}\0${entry.size}\0${entry.linkTarget}\0${entry.targetKind}\0${entry.resolvedTarget}`)
1847
+ ].join('\n');
1903
1848
  return createHash('sha256').update(content).digest('hex');
1904
1849
  }
1905
1850
  function validatePublishedSourceWorkspacePath(path) {
@@ -32,6 +32,7 @@ export class WorkspaceChangeService {
32
32
  const existing = this.state.changesReads.get(workspace.id);
33
33
  if (existing !== undefined)
34
34
  return existing;
35
+ // Invalidation may arrive during this shared read; its old result must not repopulate the cache.
35
36
  const version = this.state.changesVersions.get(workspace.id) ?? 0;
36
37
  const request = this.state.changesLimiter.run(async () => {
37
38
  const startedAt = performance.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.119",
3
+ "version": "0.9.121",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -30,9 +30,9 @@
30
30
  "npm": "11.12.1",
31
31
  "ws": "^8.21.3",
32
32
  "zod": "4.4.3",
33
- "@myagentroam/protocol": "0.9.119",
34
- "@myagentroam/agent": "0.9.119",
35
- "@myagentroam/code-navigation": "0.9.119"
33
+ "@myagentroam/agent": "0.9.121",
34
+ "@myagentroam/code-navigation": "0.9.121",
35
+ "@myagentroam/protocol": "0.9.121"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/ws": "^8.18.1"