veryfront 0.1.1069 → 0.1.1072

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/esm/_dnt.polyfills.d.ts +13 -13
  2. package/esm/_dnt.polyfills.d.ts.map +1 -1
  3. package/esm/_dnt.polyfills.js +11 -11
  4. package/esm/deno.js +2 -2
  5. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  6. package/esm/src/agent/runtime/index.js +26 -3
  7. package/esm/src/agent/runtime/tool-result-continuation.d.ts.map +1 -1
  8. package/esm/src/agent/runtime/tool-result-continuation.js +10 -2
  9. package/esm/src/cache/cache-key-builder.d.ts +30 -0
  10. package/esm/src/cache/cache-key-builder.d.ts.map +1 -1
  11. package/esm/src/cache/cache-key-builder.js +65 -0
  12. package/esm/src/cache/keys/builders/render.d.ts +1 -1
  13. package/esm/src/cache/keys/builders/render.d.ts.map +1 -1
  14. package/esm/src/cache/keys/builders/render.js +7 -4
  15. package/esm/src/observability/tracing/otlp-setup.d.ts +2 -0
  16. package/esm/src/observability/tracing/otlp-setup.d.ts.map +1 -1
  17. package/esm/src/observability/tracing/otlp-setup.js +7 -1
  18. package/esm/src/platform/adapters/fs/veryfront/proxy-manager.d.ts +2 -2
  19. package/esm/src/platform/adapters/fs/veryfront/proxy-manager.d.ts.map +1 -1
  20. package/esm/src/platform/adapters/fs/veryfront/proxy-manager.js +12 -6
  21. package/esm/src/registry/project-scoped-registry-manager.d.ts +31 -1
  22. package/esm/src/registry/project-scoped-registry-manager.d.ts.map +1 -1
  23. package/esm/src/registry/project-scoped-registry-manager.js +241 -21
  24. package/esm/src/server/handlers/request/api/project-discovery.d.ts.map +1 -1
  25. package/esm/src/server/handlers/request/api/project-discovery.js +45 -41
  26. package/esm/src/tool/registry.d.ts.map +1 -1
  27. package/esm/src/tool/registry.js +14 -9
  28. package/esm/src/tool/remote-mcp.d.ts.map +1 -1
  29. package/esm/src/tool/remote-mcp.js +14 -2
  30. package/esm/src/utils/version-constant.d.ts +1 -1
  31. package/esm/src/utils/version-constant.js +1 -1
  32. package/esm/src/workflow/executor/step-executor.d.ts.map +1 -1
  33. package/esm/src/workflow/executor/step-executor.js +24 -4
  34. package/package.json +5 -5
@@ -22,15 +22,89 @@
22
22
  *
23
23
  * @module
24
24
  */
25
- import { tryGetCacheKeyContext } from "../cache/cache-key-builder.js";
25
+ import { tryGetRegistryScopeId } from "../cache/cache-key-builder.js";
26
26
  import { agentLogger } from "../utils/logger/logger.js";
27
+ import { AsyncLocalStorage } from "node:async_hooks";
27
28
  const DEFAULT_SCOPE_ID = "__default__";
29
+ const registryTransactionStorage = new AsyncLocalStorage();
30
+ const registryTransactionLocks = new Map();
31
+ async function acquireRegistryTransactionLock(scopeId) {
32
+ const previous = registryTransactionLocks.get(scopeId) ?? Promise.resolve();
33
+ const gate = Promise.withResolvers();
34
+ const current = previous.then(() => gate.promise);
35
+ registryTransactionLocks.set(scopeId, current);
36
+ await previous;
37
+ let released = false;
38
+ return () => {
39
+ if (released)
40
+ return;
41
+ released = true;
42
+ gate.resolve();
43
+ if (registryTransactionLocks.get(scopeId) === current) {
44
+ registryTransactionLocks.delete(scopeId);
45
+ }
46
+ };
47
+ }
28
48
  function buildRegistryScopeId() {
29
- const cacheContext = tryGetCacheKeyContext();
30
- if (!cacheContext) {
31
- return DEFAULT_SCOPE_ID;
49
+ // tryGetRegistryScopeId() returns a project-isolated key even when
50
+ // tryGetCacheKeyContext() would return null (e.g. control-plane runs for an
51
+ // environment source without a pinned releaseId). Without this, all such
52
+ // runs collapse to "__default__", so concurrent projects can overwrite one
53
+ // another's registered primitives.
54
+ return tryGetRegistryScopeId() ?? DEFAULT_SCOPE_ID;
55
+ }
56
+ /**
57
+ * Stage project-scoped registry mutations and publish them as one synchronous
58
+ * commit after the callback succeeds. Reads inside the callback see staged
59
+ * state; concurrent requests keep seeing the previous live state.
60
+ * Transactions for the same scope are serialized. Live writes made outside
61
+ * the transaction while discovery is in flight are journaled alongside staged
62
+ * mutations and replayed in call order, preserving the previous immediate-
63
+ * mutation semantics without exposing a partially discovered generation.
64
+ * Use this for complete discovery generations, not incremental updates.
65
+ *
66
+ * Nested calls participate in the existing transaction. If a nested tenant
67
+ * context changes the registry scope, the first registry access throws rather
68
+ * than committing data into the wrong tenant.
69
+ */
70
+ export async function runWithRegistryTransaction(fn) {
71
+ const existing = registryTransactionStorage.getStore();
72
+ if (existing?.state === "active")
73
+ return await fn();
74
+ const targetScopeId = buildRegistryScopeId();
75
+ const releaseLock = await acquireRegistryTransactionLock(targetScopeId);
76
+ const transaction = {
77
+ targetScopeId,
78
+ stages: new Map(),
79
+ state: "active",
80
+ };
81
+ try {
82
+ return await registryTransactionStorage.run(transaction, async () => {
83
+ try {
84
+ const result = await fn();
85
+ // Prepare every manager before publishing any of them. Publication is
86
+ // synchronous, so no request can observe a partial generation.
87
+ const publications = Array.from(transaction.stages.values(), (stage) => stage.prepare());
88
+ for (const publication of publications) {
89
+ publication.publish();
90
+ }
91
+ transaction.state = "committed";
92
+ transaction.stages.clear();
93
+ return result;
94
+ }
95
+ catch (error) {
96
+ transaction.state = "aborted";
97
+ for (const stage of transaction.stages.values()) {
98
+ stage.abort();
99
+ }
100
+ transaction.stages.clear();
101
+ throw error;
102
+ }
103
+ });
104
+ }
105
+ finally {
106
+ releaseLock();
32
107
  }
33
- return `${cacheContext.projectId}:${cacheContext.mode}:${cacheContext.versionId}`;
34
108
  }
35
109
  /**
36
110
  * Base class for project-scoped registries.
@@ -39,10 +113,34 @@ function buildRegistryScopeId() {
39
113
  */
40
114
  export class ProjectScopedRegistryManager {
41
115
  registryName;
116
+ options;
42
117
  registriesByScope = new Map();
43
118
  sharedRegistry = new Map();
44
- constructor(registryName) {
119
+ activeStagesByScope = new Map();
120
+ constructor(registryName, options = {}) {
45
121
  this.registryName = registryName;
122
+ this.options = options;
123
+ }
124
+ validateRegistration(registry, id, incoming) {
125
+ if (!registry.has(id))
126
+ return;
127
+ this.options.validateRegistration?.(id, registry.get(id), incoming);
128
+ }
129
+ applyMutation(registry, mutation, validateRegistration = false) {
130
+ switch (mutation.type) {
131
+ case "clear":
132
+ registry.clear();
133
+ break;
134
+ case "delete":
135
+ registry.delete(mutation.id);
136
+ break;
137
+ case "set":
138
+ if (validateRegistration) {
139
+ this.validateRegistration(registry, mutation.id, mutation.item);
140
+ }
141
+ registry.set(mutation.id, mutation.item);
142
+ break;
143
+ }
46
144
  }
47
145
  /**
48
146
  * Get the current project ID from AsyncLocalStorage context.
@@ -51,6 +149,82 @@ export class ProjectScopedRegistryManager {
51
149
  getCurrentScopeId() {
52
150
  return buildRegistryScopeId();
53
151
  }
152
+ /** Return the transaction-local stage for this manager and scope. */
153
+ getTransactionStage(scopeId) {
154
+ const transaction = registryTransactionStorage.getStore();
155
+ if (!transaction)
156
+ return undefined;
157
+ if (transaction.state === "committed")
158
+ return undefined;
159
+ if (transaction.state === "aborted") {
160
+ throw new Error(`[${this.registryName}] Registry transaction already aborted for scope ` +
161
+ `"${transaction.targetScopeId}"`);
162
+ }
163
+ if (scopeId !== transaction.targetScopeId) {
164
+ throw new Error(`[${this.registryName}] Registry scope changed during transaction: ` +
165
+ `expected "${transaction.targetScopeId}", got "${scopeId}"`);
166
+ }
167
+ const existing = transaction.stages.get(this);
168
+ if (existing)
169
+ return existing;
170
+ const baseRegistry = new Map(this.registriesByScope.get(scopeId));
171
+ const registry = new Map(baseRegistry);
172
+ const validationRegistry = new Map(baseRegistry);
173
+ const mutations = [];
174
+ let closed = false;
175
+ const close = () => {
176
+ if (closed)
177
+ return;
178
+ closed = true;
179
+ const activeStages = this.activeStagesByScope.get(scopeId);
180
+ activeStages?.delete(stage);
181
+ if (activeStages?.size === 0)
182
+ this.activeStagesByScope.delete(scopeId);
183
+ };
184
+ const stage = {
185
+ registry,
186
+ validateRegistration: (id, incoming) => {
187
+ this.validateRegistration(validationRegistry, id, incoming);
188
+ },
189
+ record: (mutation) => {
190
+ mutations.push(mutation);
191
+ this.applyMutation(validationRegistry, mutation);
192
+ },
193
+ prepare: () => {
194
+ const replacement = new Map(baseRegistry);
195
+ for (const mutation of mutations) {
196
+ this.applyMutation(replacement, mutation, true);
197
+ }
198
+ return {
199
+ publish: () => {
200
+ close();
201
+ if (replacement.size === 0) {
202
+ this.registriesByScope.delete(scopeId);
203
+ }
204
+ else {
205
+ this.registriesByScope.set(scopeId, replacement);
206
+ }
207
+ },
208
+ };
209
+ },
210
+ abort: close,
211
+ };
212
+ transaction.stages.set(this, stage);
213
+ const activeStages = this.activeStagesByScope.get(scopeId) ?? new Set();
214
+ activeStages.add(stage);
215
+ this.activeStagesByScope.set(scopeId, activeStages);
216
+ return stage;
217
+ }
218
+ /** Record a live mutation in any in-flight transaction for this scope. */
219
+ recordLiveMutation(scopeId, mutation) {
220
+ for (const stage of this.activeStagesByScope.get(scopeId) ?? []) {
221
+ stage.record(mutation);
222
+ }
223
+ }
224
+ /** Read the active registry, routing transaction access to its staged copy. */
225
+ getActiveScopeRegistry(scopeId) {
226
+ return this.getTransactionStage(scopeId)?.registry ?? this.registriesByScope.get(scopeId);
227
+ }
54
228
  /**
55
229
  * Get or create registry for a specific project.
56
230
  */
@@ -67,11 +241,21 @@ export class ProjectScopedRegistryManager {
67
241
  */
68
242
  register(id, item) {
69
243
  const scopeId = this.getCurrentScopeId();
70
- const registry = this.getScopeRegistry(scopeId);
244
+ const stage = this.getTransactionStage(scopeId);
245
+ const registry = stage?.registry ?? this.getScopeRegistry(scopeId);
246
+ if (stage)
247
+ stage.validateRegistration(id, item);
248
+ else
249
+ this.validateRegistration(registry, id, item);
71
250
  if (registry.has(id)) {
72
251
  agentLogger.debug(`[${this.registryName}] "${id}" already registered for scope ${scopeId}. Overwriting.`);
73
252
  }
74
253
  registry.set(id, item);
254
+ const mutation = { type: "set", id, item };
255
+ if (stage)
256
+ stage.record(mutation);
257
+ else
258
+ this.recordLiveMutation(scopeId, mutation);
75
259
  agentLogger.debug(`[${this.registryName}] Registered "${id}" for scope ${scopeId}`);
76
260
  }
77
261
  /**
@@ -79,6 +263,9 @@ export class ProjectScopedRegistryManager {
79
263
  * Use for framework-provided tools, not user-defined ones.
80
264
  */
81
265
  registerShared(id, item) {
266
+ // Shared framework infrastructure is intentionally process-wide and is
267
+ // published immediately even inside a project transaction. Project
268
+ // discovery must never use this method for tenant-owned definitions.
82
269
  if (this.sharedRegistry.has(id)) {
83
270
  agentLogger.debug(`[${this.registryName}] Shared "${id}" already registered. Overwriting.`);
84
271
  }
@@ -91,7 +278,7 @@ export class ProjectScopedRegistryManager {
91
278
  */
92
279
  get(id) {
93
280
  const scopeId = this.getCurrentScopeId();
94
- return this.registriesByScope.get(scopeId)?.get(id) ?? this.sharedRegistry.get(id);
281
+ return this.getActiveScopeRegistry(scopeId)?.get(id) ?? this.sharedRegistry.get(id);
95
282
  }
96
283
  /**
97
284
  * Get item registered in the current project's own scope, without falling
@@ -101,14 +288,14 @@ export class ProjectScopedRegistryManager {
101
288
  */
102
289
  getOwn(id) {
103
290
  const scopeId = this.getCurrentScopeId();
104
- return this.registriesByScope.get(scopeId)?.get(id);
291
+ return this.getActiveScopeRegistry(scopeId)?.get(id);
105
292
  }
106
293
  /**
107
294
  * Check if item exists for the current project.
108
295
  */
109
296
  has(id) {
110
297
  const scopeId = this.getCurrentScopeId();
111
- return (this.registriesByScope.get(scopeId)?.has(id) ?? false) ||
298
+ return (this.getActiveScopeRegistry(scopeId)?.has(id) ?? false) ||
112
299
  this.sharedRegistry.has(id);
113
300
  }
114
301
  /**
@@ -116,7 +303,7 @@ export class ProjectScopedRegistryManager {
116
303
  */
117
304
  getAllIds() {
118
305
  const scopeId = this.getCurrentScopeId();
119
- const projectIds = this.registriesByScope.get(scopeId)?.keys() ?? [];
306
+ const projectIds = this.getActiveScopeRegistry(scopeId)?.keys() ?? [];
120
307
  const sharedIds = this.sharedRegistry.keys();
121
308
  return Array.from(new Set([...projectIds, ...sharedIds]));
122
309
  }
@@ -125,7 +312,7 @@ export class ProjectScopedRegistryManager {
125
312
  */
126
313
  getAll() {
127
314
  const scopeId = this.getCurrentScopeId();
128
- const projectRegistry = this.registriesByScope.get(scopeId);
315
+ const projectRegistry = this.getActiveScopeRegistry(scopeId);
129
316
  if (!projectRegistry)
130
317
  return new Map(this.sharedRegistry);
131
318
  const result = new Map(this.sharedRegistry);
@@ -138,28 +325,50 @@ export class ProjectScopedRegistryManager {
138
325
  */
139
326
  delete(id) {
140
327
  const scopeId = this.getCurrentScopeId();
141
- const registry = this.registriesByScope.get(scopeId);
142
- if (!registry?.has(id))
328
+ const stage = this.getTransactionStage(scopeId);
329
+ const registry = stage?.registry ?? this.registriesByScope.get(scopeId);
330
+ const existed = registry?.has(id) ?? false;
331
+ if (!existed && !stage && !this.activeStagesByScope.has(scopeId))
143
332
  return false;
144
- registry.delete(id);
333
+ registry?.delete(id);
334
+ const mutation = { type: "delete", id };
335
+ if (stage)
336
+ stage.record(mutation);
337
+ else
338
+ this.recordLiveMutation(scopeId, mutation);
145
339
  agentLogger.debug(`[${this.registryName}] Deleted "${id}" from scope ${scopeId}`);
146
- return true;
340
+ return existed;
147
341
  }
148
342
  /**
149
343
  * Clear all items for the current project.
150
344
  */
151
345
  clear() {
152
- this.clearProject(this.getCurrentScopeId());
346
+ const scopeId = this.getCurrentScopeId();
347
+ const stage = this.getTransactionStage(scopeId);
348
+ if (stage) {
349
+ stage.registry.clear();
350
+ stage.record({ type: "clear" });
351
+ return;
352
+ }
353
+ this.clearProject(scopeId);
153
354
  }
154
355
  /**
155
356
  * Clear a specific project's registry.
156
357
  */
157
358
  clearProject(projectId) {
359
+ const transaction = registryTransactionStorage.getStore();
360
+ if (transaction && transaction.state !== "committed") {
361
+ throw new Error(`[${this.registryName}] clearProject() is not supported during a registry transaction`);
362
+ }
158
363
  let cleared = false;
159
- for (const scopeId of Array.from(this.registriesByScope.keys())) {
364
+ const scopeIds = new Set([
365
+ ...this.registriesByScope.keys(),
366
+ ...this.activeStagesByScope.keys(),
367
+ ]);
368
+ for (const scopeId of scopeIds) {
160
369
  if (scopeId === projectId || scopeId.startsWith(`${projectId}:`)) {
161
- this.registriesByScope.delete(scopeId);
162
- cleared = true;
370
+ cleared = this.registriesByScope.delete(scopeId) || cleared;
371
+ this.recordLiveMutation(scopeId, { type: "clear" });
163
372
  }
164
373
  }
165
374
  if (cleared) {
@@ -170,6 +379,17 @@ export class ProjectScopedRegistryManager {
170
379
  * Clear everything (for testing).
171
380
  */
172
381
  clearAll() {
382
+ const transaction = registryTransactionStorage.getStore();
383
+ if (transaction && transaction.state !== "committed") {
384
+ throw new Error(`[${this.registryName}] clearAll() is not supported during a registry transaction`);
385
+ }
386
+ const scopeIds = new Set([
387
+ ...this.registriesByScope.keys(),
388
+ ...this.activeStagesByScope.keys(),
389
+ ]);
390
+ for (const scopeId of scopeIds) {
391
+ this.recordLiveMutation(scopeId, { type: "clear" });
392
+ }
173
393
  this.registriesByScope.clear();
174
394
  this.sharedRegistry.clear();
175
395
  agentLogger.debug(`[${this.registryName}] Cleared all registries`);
@@ -185,7 +405,7 @@ export class ProjectScopedRegistryManager {
185
405
  projectCount: this.registriesByScope.size,
186
406
  sharedCount: this.sharedRegistry.size,
187
407
  totalItems,
188
- currentProjectItems: this.registriesByScope.get(scopeId)?.size ?? 0,
408
+ currentProjectItems: this.getActiveScopeRegistry(scopeId)?.size ?? 0,
189
409
  };
190
410
  }
191
411
  }
@@ -1 +1 @@
1
- {"version":3,"file":"project-discovery.d.ts","sourceRoot":"","sources":["../../../../../../src/src/server/handlers/request/api/project-discovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAKtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAyHrD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAgF1F"}
1
+ {"version":3,"file":"project-discovery.d.ts","sourceRoot":"","sources":["../../../../../../src/src/server/handlers/request/api/project-discovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAMtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAyHrD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAmF1F"}
@@ -1,6 +1,7 @@
1
1
  import { serverLogger } from "../../../../utils/index.js";
2
2
  import { clearTrackedAgents, createProjectDiscoveryConfig } from "../../../../discovery/index.js";
3
- import { tryGetCacheKeyContext } from "../../../../cache/cache-key-builder.js";
3
+ import { tryGetRegistryScopeContext } from "../../../../cache/cache-key-builder.js";
4
+ import { runWithRegistryTransaction } from "../../../../registry/project-scoped-registry-manager.js";
4
5
  import { sanitizeUrlCredentials } from "../../../../utils/logger/redact.js";
5
6
  const logger = serverLogger.component("api-wrapper");
6
7
  const discoveredProjects = new Map();
@@ -61,9 +62,9 @@ function summarizeDiscoveryFailures(errors, projectDir) {
61
62
  }
62
63
  /** Build a discovery cache key that incorporates the release/version. */
63
64
  function discoveryKey(ctx) {
64
- const cacheContext = tryGetCacheKeyContext();
65
- if (cacheContext) {
66
- return `${cacheContext.projectId}:${cacheContext.mode}:${cacheContext.versionId}`;
65
+ const registryScope = tryGetRegistryScopeContext();
66
+ if (registryScope) {
67
+ return registryScope.scopeId;
67
68
  }
68
69
  const slug = ctx.projectSlug ?? ctx.projectDir;
69
70
  const environment = ctx.enriched?.environment ?? ctx.resolvedEnvironment ??
@@ -76,9 +77,9 @@ function discoveryKey(ctx) {
76
77
  return `${slug}:preview:${branch}`;
77
78
  }
78
79
  function shouldCacheCompletedDiscovery(ctx) {
79
- const cacheContext = tryGetCacheKeyContext();
80
- if (cacheContext) {
81
- return cacheContext.mode === "production";
80
+ const registryScope = tryGetRegistryScopeContext();
81
+ if (registryScope) {
82
+ return registryScope.immutable;
82
83
  }
83
84
  const environment = ctx.enriched?.environment ?? ctx.resolvedEnvironment ??
84
85
  (ctx.releaseId ? "production" : "preview");
@@ -101,41 +102,44 @@ export async function ensureProjectDiscovery(ctx) {
101
102
  const { clearTranspileCache, discoverAll } = await import("../../../../discovery/index.js");
102
103
  const { agentRegistry } = await import("../../../../agent/composition/composition.js");
103
104
  const { toolRegistry } = await import("../../../../tool/registry.js");
104
- // Clear stale entries for this project scope before re-discovery.
105
- // This prevents agents/tools removed in a new release from lingering.
106
- clearTrackedAgents();
107
- clearTranspileCache();
108
- agentRegistry.clear();
109
- toolRegistry.clear();
110
- const discoveryOptions = createProjectDiscoveryConfig({
111
- projectDir: ctx.projectDir,
112
- config: ctx.config,
113
- fsAdapter: ctx.adapter.fs,
114
- });
115
- const result = await discoverAll(discoveryOptions);
116
- const shouldWarnOnEmptyAiDiscovery = discoveryOptions.toolDirs.length > 0 ||
117
- discoveryOptions.agentDirs.length > 0;
118
- const logData = {
119
- projectSlug: ctx.projectSlug,
120
- releaseId: ctx.releaseId,
121
- agents: result.agents.size,
122
- tools: result.tools.size,
123
- errors: result.errors.length,
124
- };
125
- if (result.errors.length > 0) {
126
- logger.warn("Primitive discovery completed with errors", {
127
- ...logData,
128
- failures: summarizeDiscoveryFailures(result.errors, ctx.projectDir),
129
- omittedErrors: Math.max(0, result.errors.length - MAX_DISCOVERY_FAILURES_TO_LOG),
105
+ return await runWithRegistryTransaction(async () => {
106
+ // Clear stale entries in a transaction-local copy. Concurrent runs keep
107
+ // using the prior live registry until discovery succeeds and the staged
108
+ // replacement is committed atomically.
109
+ clearTrackedAgents();
110
+ clearTranspileCache();
111
+ agentRegistry.clear();
112
+ toolRegistry.clear();
113
+ const discoveryOptions = createProjectDiscoveryConfig({
114
+ projectDir: ctx.projectDir,
115
+ config: ctx.config,
116
+ fsAdapter: ctx.adapter.fs,
130
117
  });
131
- }
132
- else if (result.agents.size === 0 && result.tools.size === 0 && shouldWarnOnEmptyAiDiscovery) {
133
- logger.info("Primitive discovery found 0 agents and 0 tools", logData);
134
- }
135
- else {
136
- logger.info("Primitive discovery completed", logData);
137
- }
138
- return result;
118
+ const result = await discoverAll(discoveryOptions);
119
+ const shouldWarnOnEmptyAiDiscovery = discoveryOptions.toolDirs.length > 0 ||
120
+ discoveryOptions.agentDirs.length > 0;
121
+ const logData = {
122
+ projectSlug: ctx.projectSlug,
123
+ releaseId: ctx.releaseId,
124
+ agents: result.agents.size,
125
+ tools: result.tools.size,
126
+ errors: result.errors.length,
127
+ };
128
+ if (result.errors.length > 0) {
129
+ logger.warn("Primitive discovery completed with errors", {
130
+ ...logData,
131
+ failures: summarizeDiscoveryFailures(result.errors, ctx.projectDir),
132
+ omittedErrors: Math.max(0, result.errors.length - MAX_DISCOVERY_FAILURES_TO_LOG),
133
+ });
134
+ }
135
+ else if (result.agents.size === 0 && result.tools.size === 0 && shouldWarnOnEmptyAiDiscovery) {
136
+ logger.info("Primitive discovery found 0 agents and 0 tools", logData);
137
+ }
138
+ else {
139
+ logger.info("Primitive discovery completed", logData);
140
+ }
141
+ return result;
142
+ });
139
143
  })(),
140
144
  };
141
145
  discoveredProjects.set(key, discovery);
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/src/tool/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAiB7E,cAAM,iBAAkB,SAAQ,oBAAoB,CAAC,IAAI,CAAC;IAC/C,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI;IAgB/C,mBAAmB,IAAI,cAAc,EAAE;CAGxC;AAED,kCAAkC;AAClC,eAAO,MAAM,YAAY,mBAAqC,CAAC;AAE/D,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,IAAI,GAAG,cAAc,CAenE"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/src/tool/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AA2B7E,cAAM,iBAAkB,SAAQ,oBAAoB,CAAC,IAAI,CAAC;IAC/C,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI;IAW/C,mBAAmB,IAAI,cAAc,EAAE;CAGxC;AAED,kCAAkC;AAClC,eAAO,MAAM,YAAY,mBAAqC,CAAC;AAE/D,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,IAAI,GAAG,cAAc,CAenE"}
@@ -3,7 +3,6 @@ import { agentLogger } from "../utils/logger/logger.js";
3
3
  import { ScopedRegistryFacade } from "../registry/scoped-registry-facade.js";
4
4
  import { ProjectScopedRegistryManager } from "../registry/project-scoped-registry-manager.js";
5
5
  import { TOOL_ID_CONFLICT } from "../errors/error-registry/agent.js";
6
- const toolManager = new ProjectScopedRegistryManager("tool");
7
6
  /**
8
7
  * Returns true when `incoming` is considered the same definition as `existing`:
9
8
  * same object reference, or matching id + description. Equivalent definitions
@@ -14,17 +13,23 @@ function isSameToolDefinition(existing, incoming) {
14
13
  return existing === incoming ||
15
14
  (existing.id === incoming.id && existing.description === incoming.description);
16
15
  }
16
+ function validateToolRegistration(id, existing, incoming) {
17
+ if (isSameToolDefinition(existing, incoming))
18
+ return;
19
+ throw TOOL_ID_CONFLICT.create({
20
+ detail: `Tool "${id}" is already registered with a different definition. Use a unique tool ID or rename one of the conflicting tools.`,
21
+ });
22
+ }
23
+ const toolManager = new ProjectScopedRegistryManager("tool", {
24
+ validateRegistration: validateToolRegistration,
25
+ });
17
26
  class ToolRegistryClass extends ScopedRegistryFacade {
18
27
  register(id, item) {
19
- // Conflict-check against the project's own scope only: a project tool is
20
- // allowed to shadow a shared/framework tool with the same ID.
28
+ // Equivalent-registration diagnostics inspect the project scope only;
29
+ // the manager enforces conflicts here and again against journaled order.
30
+ // Shared/framework tools remain intentionally shadowable.
21
31
  const existing = this.getOwn(id);
22
- if (existing !== undefined && !isSameToolDefinition(existing, item)) {
23
- throw TOOL_ID_CONFLICT.create({
24
- detail: `Tool "${id}" is already registered with a different definition. Use a unique tool ID or rename one of the conflicting tools.`,
25
- });
26
- }
27
- if (existing !== undefined && existing !== item) {
32
+ if (existing !== undefined && existing !== item && isSameToolDefinition(existing, item)) {
28
33
  agentLogger.debug(`[tool] "${id}" re-registered with equivalent definition; replacing.`);
29
34
  }
30
35
  super.register(id, item);
@@ -1 +1 @@
1
- {"version":3,"file":"remote-mcp.d.ts","sourceRoot":"","sources":["../../../src/src/tool/remote-mcp.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,oBAAoB,EAAE,MAAM,YAAY,CAAC;AASzF,KAAK,eAAe,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,oBAAoB,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AAEnF,oDAAoD;AACpD,MAAM,WAAW,yBAAyB;IACxC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAClC,OAAO,CAAC,EAAE,eAAe,CAAC,WAAW,GAAG,SAAS,CAAC,CAAC;IACnD,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAqbD,qCAAqC;AACrC,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,yBAAyB,GAChC,gBAAgB,CA0FlB"}
1
+ {"version":3,"file":"remote-mcp.d.ts","sourceRoot":"","sources":["../../../src/src/tool/remote-mcp.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,oBAAoB,EAAE,MAAM,YAAY,CAAC;AASzF,KAAK,eAAe,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,oBAAoB,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AAEnF,oDAAoD;AACpD,MAAM,WAAW,yBAAyB;IACxC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAClC,OAAO,CAAC,EAAE,eAAe,CAAC,WAAW,GAAG,SAAS,CAAC,CAAC;IACnD,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAucD,qCAAqC;AACrC,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,yBAAyB,GAChC,gBAAgB,CA0FlB"}
@@ -139,6 +139,18 @@ function normalizeKnownToolError(value, toolName, endpoint, context) {
139
139
  message: `${label} needs to be reconnected before this tool can run.`,
140
140
  };
141
141
  }
142
+ function preserveToolExecutionErrorMarker(value) {
143
+ if (isRecord(value) && !Array.isArray(value)) {
144
+ return hasToolExecutionErrorMarker(value) ? value : { ...value, isError: true };
145
+ }
146
+ return {
147
+ isError: true,
148
+ message: typeof value === "string" && value.trim().length > 0
149
+ ? value
150
+ : "Remote MCP tool returned an error",
151
+ ...(value === undefined ? {} : { output: value }),
152
+ };
153
+ }
142
154
  function isReconnectRequiredToolOutput(value) {
143
155
  return isRecord(value) && value.error === "reconnect_required";
144
156
  }
@@ -296,7 +308,7 @@ function normalizeCallToolResult(input) {
296
308
  const errorBody = "structuredContent" in result
297
309
  ? result.structuredContent
298
310
  : parseJsonText(text) ?? { error: "tool_error", message: text };
299
- return normalizeKnownToolError(errorBody, input.toolName, input.endpoint, input.context);
311
+ return preserveToolExecutionErrorMarker(normalizeKnownToolError(errorBody, input.toolName, input.endpoint, input.context));
300
312
  }
301
313
  if ("structuredContent" in result) {
302
314
  return result.structuredContent;
@@ -305,7 +317,7 @@ function normalizeCallToolResult(input) {
305
317
  }
306
318
  if (isError) {
307
319
  const errorBody = "structuredContent" in result ? result.structuredContent : result;
308
- return normalizeKnownToolError(errorBody, input.toolName, input.endpoint, input.context);
320
+ return preserveToolExecutionErrorMarker(normalizeKnownToolError(errorBody, input.toolName, input.endpoint, input.context));
309
321
  }
310
322
  if ("structuredContent" in result) {
311
323
  return result.structuredContent;
@@ -1,3 +1,3 @@
1
1
  /** Shared version value. */
2
- export declare const VERSION = "0.1.1069";
2
+ export declare const VERSION = "0.1.1072";
3
3
  //# sourceMappingURL=version-constant.d.ts.map
@@ -1,4 +1,4 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
3
  /** Shared version value. */
4
- export const VERSION = "0.1.1069";
4
+ export const VERSION = "0.1.1072";
@@ -1 +1 @@
1
- {"version":3,"file":"step-executor.d.ts","sourceRoot":"","sources":["../../../../src/src/workflow/executor/step-executor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"step-executor.d.ts","sourceRoot":"","sources":["../../../../src/src/workflow/executor/step-executor.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import { runWithCacheKeyContext, } from "../../cache/cache-key-builder.js";
2
+ import { runWithCacheKeyContext, runWithoutCacheKeyContext, } from "../../cache/cache-key-builder.js";
3
+ import { runWithRequestContext } from "../../platform/adapters/fs/veryfront/request-context.js";
3
4
  import { ensureError } from "../../errors/veryfront-error.js";
4
5
  import { isVeryfrontError } from "../../errors/http-error.js";
5
6
  import { AGENT_NOT_FOUND, INITIALIZATION_ERROR, INVALID_ARGUMENT, ORCHESTRATION_ERROR, RESOURCE_NOT_FOUND, TIMEOUT_ERROR, } from "../../errors/error-registry.js";
@@ -22,10 +23,16 @@ export function getWorkflowTenant() {
22
23
  }
23
24
  function cacheKeyContextFromWorkflowTenant(tenant) {
24
25
  const mode = tenant.productionMode ? "production" : "preview";
26
+ // Environment sources are mutable and have no immutable version segment.
27
+ // A synthetic "latest" distributed-cache bucket can mix different source
28
+ // snapshots, so these tenants use request context for registry isolation and
29
+ // deliberately skip distributed caching.
30
+ if (mode === "production" && !tenant.releaseId)
31
+ return null;
25
32
  return {
26
- projectId: tenant.projectId || tenant.projectSlug || "default",
33
+ projectId: tenant.projectId || tenant.projectSlug,
27
34
  mode,
28
- versionId: mode === "production" ? (tenant.releaseId || "latest") : (tenant.branch || "main"),
35
+ versionId: mode === "production" ? tenant.releaseId : (tenant.branch || "main"),
29
36
  };
30
37
  }
31
38
  /**
@@ -35,7 +42,20 @@ function cacheKeyContextFromWorkflowTenant(tenant) {
35
42
  export function runWithWorkflowTenant(tenant, fn) {
36
43
  if (!tenant)
37
44
  return fn();
38
- return workflowTenantStorage.run(tenant, () => runWithCacheKeyContext(cacheKeyContextFromWorkflowTenant(tenant), fn));
45
+ return workflowTenantStorage.run(tenant, () => runWithRequestContext({
46
+ projectSlug: tenant.projectSlug,
47
+ token: tenant.token,
48
+ projectId: tenant.projectId,
49
+ productionMode: tenant.productionMode,
50
+ releaseId: tenant.releaseId,
51
+ branch: tenant.branch,
52
+ environmentName: tenant.environmentName,
53
+ }, () => {
54
+ const cacheContext = cacheKeyContextFromWorkflowTenant(tenant);
55
+ return cacheContext
56
+ ? runWithCacheKeyContext(cacheContext, fn)
57
+ : runWithoutCacheKeyContext(fn);
58
+ }));
39
59
  }
40
60
  /** Default initial delay before first retry attempt */
41
61
  const DEFAULT_RETRY_INITIAL_DELAY_MS = 1_000;