@dev-mainsequence/codex-plugin 0.7.10 → 0.7.12

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.
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.projectToolCatalogPath = exports.projectToolConnectionLabel = exports.isProjectToolName = exports.MAX_PROJECT_TOOL_TIMEOUT_MS = exports.DEFAULT_PROJECT_REQUEST_TIMEOUT_MS = exports.PROJECT_TOOL_TIMEOUT_HINT = exports.MAX_PROJECT_TOOLS_PER_CONNECTION = exports.PROJECT_TOOL_PREFIX = void 0;
36
+ exports.projectToolCatalogPath = exports.projectToolConnectionLabel = exports.isProjectToolName = exports.DEFAULT_PROJECT_REFRESH_BUDGET_MS = exports.MAX_PROJECT_TOOL_TIMEOUT_MS = exports.DEFAULT_PROJECT_REQUEST_TIMEOUT_MS = exports.PROJECT_TOOL_TIMEOUT_HINT = exports.MAX_PROJECT_TOOLS_PER_CONNECTION = exports.PROJECT_TOOL_PREFIX = void 0;
37
37
  exports.projectToolName = projectToolName;
38
38
  exports.projectToolConnection = projectToolConnection;
39
39
  exports.projectToolDefinition = projectToolDefinition;
@@ -51,12 +51,16 @@ const path = __importStar(require("node:path"));
51
51
  const node_crypto_1 = require("node:crypto");
52
52
  const models_1 = require("./models.js");
53
53
  const stateDirectory_1 = require("./stateDirectory.js");
54
+ const repositories_1 = require("./repositories.js");
54
55
  exports.PROJECT_TOOL_PREFIX = 'ms_';
55
56
  exports.MAX_PROJECT_TOOLS_PER_CONNECTION = 80;
56
57
  /** A project may advertise how long one of its tools can take; the client honours it up to the cap. */
57
58
  exports.PROJECT_TOOL_TIMEOUT_HINT = 'mainsequence.ai/timeout-seconds/v1';
58
59
  exports.DEFAULT_PROJECT_REQUEST_TIMEOUT_MS = 120_000;
59
60
  exports.MAX_PROJECT_TOOL_TIMEOUT_MS = 300_000;
61
+ /** A whole refresh, every connection included: a cold project answers `initialize` in up to 120 s, and a
62
+ * host must not hold the shared lock for the 13 minutes once measured. */
63
+ exports.DEFAULT_PROJECT_REFRESH_BUDGET_MS = 300_000;
60
64
  const CATALOG_FILE = 'project-tool-catalog.json';
61
65
  const LOCK_STALE_MS = 10 * 60_000;
62
66
  const UUID = /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i;
@@ -173,12 +177,15 @@ function success(result) {
173
177
  return result.data;
174
178
  }
175
179
  /** Every saved connection in scope and its current tools: `mcp-list` (read-only), then one `mcp-tools`
176
- * per connection, two at a time (the shared transport's per-origin limit). A connection whose catalog
177
- * cannot be read now keeps its `previous` tools and is named in `unavailable`; a removed connection
178
- * loses them; connections outside the scope are carried over from `previous` unchanged. No account
179
- * snapshot is attached: a registered tool is bound to a saved connection, and the session for that
180
- * connection's backend is verified when the tool is called. */
180
+ * per connection, two at a time (the shared transport's per-origin limit), all within one budget. A
181
+ * connection whose catalog cannot be read now, or is not read before the budget runs out, stays in the
182
+ * catalog marked `unavailable` with the reason, keeping its `previous` tools when it had any, so a scope
183
+ * is never written empty and healthy when its connections exist; a removed connection loses its tools;
184
+ * connections outside the scope are carried over from `previous` unchanged. No account snapshot is
185
+ * attached: a registered tool is bound to a saved connection, and the session for that connection's
186
+ * backend is verified when the tool is called. */
181
187
  async function readProjectToolCatalog(dispatch, options = {}) {
188
+ const started = Date.now(), budgetMs = options.budgetMs ?? exports.DEFAULT_PROJECT_REFRESH_BUDGET_MS;
182
189
  const status = options.session ?? success(await dispatch({ action: 'auth-status', ...(options.backend ? { backend: options.backend } : {}) }));
183
190
  if (!status.authenticated || !status.backend)
184
191
  throw new models_1.ToolingError('NOT_AUTHENTICATED', 'Sign in to MainSequence to register saved project MCP tools.');
@@ -189,39 +196,57 @@ async function readProjectToolCatalog(dispatch, options = {}) {
189
196
  const rows = success(await dispatch({ action: 'mcp-list', backend, readOnly: true, ...environment }));
190
197
  const previous = options.previous?.connections ?? [];
191
198
  const connections = previous.filter(connection => !inScope(connection, scope)), unavailable = [];
192
- let retained = 0;
199
+ let retained = 0, expired = false, timer;
200
+ const budget = new Promise(resolve => { timer = setTimeout(() => { expired = true; resolve('budget'); }, Math.max(0, budgetMs - (Date.now() - started))); });
201
+ const seconds = budgetMs >= 1000 ? `${Math.round(budgetMs / 1000)} s` : `${budgetMs} ms`;
202
+ function unreadable(row, code, message) {
203
+ const earlier = previous.find(connection => connection.backend === backend && connection.releaseUid === row.resource_release_uid.toLowerCase());
204
+ if (earlier)
205
+ retained += earlier.tools.length;
206
+ connections.push({ ...(earlier ?? projectToolConnection(row)), unavailable: { at: new Date().toISOString(), code, message } });
207
+ unavailable.push((0, exports.projectToolConnectionLabel)(row));
208
+ }
193
209
  const pending = [...rows];
194
- await Promise.all(Array.from({ length: Math.min(2, pending.length) }, async () => {
195
- while (pending.length) {
196
- options.signal?.throwIfAborted();
197
- const row = pending.shift();
198
- try {
199
- const output = success(await dispatch({ action: 'mcp-tools', backend, releaseUid: row.resource_release_uid, ...environment }));
200
- const current = output.connection;
201
- connections.push(projectToolConnection({ ...row, name: row.name ?? current?.name, display_name: row.display_name ?? current?.display_name,
202
- mcp_connection: { ...row.mcp_connection, organization_environment_name: row.mcp_connection.organization_environment_name ?? current?.mcp_connection?.organization_environment_name } }, output.tools));
203
- }
204
- catch (error) {
205
- if (error instanceof models_1.ToolingError && error.code === 'CANCELLED')
206
- throw error;
210
+ try {
211
+ await Promise.all(Array.from({ length: Math.min(2, pending.length) }, async () => {
212
+ while (pending.length && !expired) {
207
213
  options.signal?.throwIfAborted();
208
- const earlier = previous.find(connection => connection.backend === backend && connection.releaseUid === row.resource_release_uid.toLowerCase());
209
- if (earlier) {
210
- connections.push(earlier);
211
- retained += earlier.tools.length;
214
+ const row = pending.shift();
215
+ try {
216
+ const outcome = await Promise.race([Promise.resolve(dispatch({ action: 'mcp-tools', backend, releaseUid: row.resource_release_uid, ...environment })).then(result => ({ result })), budget]);
217
+ if (outcome === 'budget') {
218
+ unreadable(row, 'TIMEOUT', `Not read within the ${seconds} refresh budget; its earlier tools stay registered until a later refresh reads it.`);
219
+ break;
220
+ }
221
+ const output = success(outcome.result), current = output.connection;
222
+ connections.push(projectToolConnection({ ...row, name: row.name ?? current?.name, display_name: row.display_name ?? current?.display_name,
223
+ mcp_connection: { ...row.mcp_connection, organization_environment_name: row.mcp_connection.organization_environment_name ?? current?.mcp_connection?.organization_environment_name } }, output.tools));
224
+ }
225
+ catch (error) {
226
+ if (error instanceof models_1.ToolingError && error.code === 'CANCELLED')
227
+ throw error;
228
+ options.signal?.throwIfAborted();
229
+ unreadable(row, error instanceof models_1.ToolingError ? error.code : 'INTERNAL_ERROR', error instanceof models_1.ToolingError ? error.message : 'The catalog could not be read.');
212
230
  }
213
- unavailable.push((0, exports.projectToolConnectionLabel)(row));
214
231
  }
215
- }
216
- }));
232
+ }));
233
+ // Whatever the budget left unread is reported, never silently dropped.
234
+ for (const row of pending)
235
+ unreadable(row, 'TIMEOUT', `Not read within the ${seconds} refresh budget; its earlier tools stay registered until a later refresh reads it.`);
236
+ }
237
+ finally {
238
+ clearTimeout(timer);
239
+ }
217
240
  connections.sort((a, b) => a.backend.localeCompare(b.backend) || a.environmentUid.localeCompare(b.environmentUid) || a.releaseUid.localeCompare(b.releaseUid));
218
- return { catalog: { version: 1, updatedAt: new Date().toISOString(), connections }, backend, unavailable, retained };
241
+ return { catalog: { version: 1, updatedAt: new Date().toISOString(), connections }, backend, unavailable, retained, durationMs: Date.now() - started };
219
242
  }
220
243
  function connectionRecord(value) {
221
244
  if (!object(value) || typeof value.backend !== 'string' || !UUID.test(String(value.releaseUid)) || !UUID.test(String(value.environmentUid)) || !Array.isArray(value.tools))
222
245
  return;
246
+ const u = value.unavailable;
223
247
  return { backend: value.backend, releaseUid: String(value.releaseUid).toLowerCase(), environmentUid: String(value.environmentUid).toLowerCase(),
224
- environmentName: text(value.environmentName), name: text(value.name), tools: normalizeTools(value.tools) };
248
+ environmentName: text(value.environmentName), name: text(value.name), tools: normalizeTools(value.tools),
249
+ ...(object(u) && typeof u.code === 'string' && typeof u.message === 'string' ? { unavailable: { at: text(u.at, 40), code: text(u.code, 60), message: text(u.message, 1000) } } : {}) };
225
250
  }
226
251
  /** Beside `mcp-connections.json`, shared by every host on the machine. */
227
252
  const projectToolCatalogPath = (env = process.env) => path.join((0, stateDirectory_1.mainSequenceStateDirectory)(env), CATALOG_FILE);
@@ -250,10 +275,12 @@ function createProjectToolCatalog(options) {
250
275
  const file = options.file ?? (0, exports.projectToolCatalogPath)(options.env), staleMs = options.staleMs ?? 15 * 60_000;
251
276
  let catalog = readProjectToolCatalogFile(file), loadedAt = mtime(), running = null;
252
277
  let current = null;
253
- // The scope of the last refresh: the current account's backend (learned from auth-status), so a
254
- // listing shows this host's connections even when another backend's are cached beside them.
255
- let known;
256
- const state = { unavailable: [], retained: 0, error: null };
278
+ // The backend this host's listing is for: given by the host, resolved by ready() the way auth-status
279
+ // resolves it, or learned from a refresh. The file is shared by every host and backend on the machine,
280
+ // so until it is known a listing shows nothing rather than another backend's tools.
281
+ let known = options.backend ? { backend: options.backend } : undefined;
282
+ let readying = null;
283
+ const state = { error: null };
257
284
  function mtime() { try {
258
285
  return fs.statSync(file).mtimeMs;
259
286
  }
@@ -268,7 +295,9 @@ function createProjectToolCatalog(options) {
268
295
  catalog = readProjectToolCatalogFile(file);
269
296
  loadedAt = now;
270
297
  }
271
- const registration = (scope) => projectToolRegistration(catalog, scope ?? known);
298
+ const none = { definitions: [], bindings: new Map() };
299
+ const registration = (scope) => (scope ?? known) ? projectToolRegistration(catalog, scope ?? known) : none;
300
+ const selected = (scope) => (catalog?.connections ?? []).filter(connection => (scope ?? known) ? inScope(connection, scope ?? known) : false);
272
301
  async function write(next) {
273
302
  await fsp.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
274
303
  const temporary = `${file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
@@ -330,16 +359,27 @@ function createProjectToolCatalog(options) {
330
359
  }
331
360
  return {
332
361
  file,
333
- /** Definitions for the host's tool list, from the newest shared cache. */
362
+ /** The backend this listing is for, resolved once without any network request (the process
363
+ * environment, then the shared preferences, then the default), as `auth-status` resolves it. A
364
+ * host calls this before its first tool list; a refresh or an explicit scope also sets it. */
365
+ ready() {
366
+ if (known)
367
+ return Promise.resolve();
368
+ readying ??= (0, repositories_1.resolveBackend)(undefined, undefined, options.env ?? process.env).then(backend => { known ??= { backend }; }, () => { });
369
+ return readying;
370
+ },
371
+ /** Definitions for the host's tool list, from the newest shared cache, for the known backend only. */
334
372
  list(scope) { reload(); return registration(scope).definitions; },
335
373
  binding(name) { reload(); return registration({}).bindings.get(name); },
336
374
  state(scope) {
337
375
  reload();
338
- const selected = scope ?? known;
376
+ const connections = selected(scope), unavailable = connections.filter(connection => connection.unavailable);
339
377
  return { count: registration(scope).definitions.length,
340
- connections: (catalog?.connections ?? []).filter(connection => inScope(connection, selected)).map(connection => ({ backend: connection.backend, releaseUid: connection.releaseUid,
341
- environmentUid: connection.environmentUid, environmentName: connection.environmentName, name: connection.name, tools: connection.tools.length })),
342
- updatedAt: catalog?.updatedAt || null, refreshing: running !== null, unavailable: state.unavailable, retained: state.retained, error: state.error };
378
+ connections: connections.map(connection => ({ backend: connection.backend, releaseUid: connection.releaseUid, environmentUid: connection.environmentUid, environmentName: connection.environmentName,
379
+ name: connection.name, tools: connection.tools.length, ...(connection.unavailable ? { unavailable: connection.unavailable } : {}) })),
380
+ updatedAt: catalog?.updatedAt || null, refreshing: running !== null,
381
+ // Read from the file, so a host whose process did not run the refresh still sees what it could not read.
382
+ unavailable: unavailable.map(connection => connection.name || connection.releaseUid), retained: unavailable.reduce((sum, connection) => sum + connection.tools.length, 0), error: state.error };
343
383
  },
344
384
  /** The state plus definitions and bindings, as the `mcp-catalog` action returns them. */
345
385
  view(scope) {
@@ -383,10 +423,8 @@ function createProjectToolCatalog(options) {
383
423
  current.locked = true;
384
424
  try {
385
425
  reload();
386
- const read = await readProjectToolCatalog(dispatch, { ...scope, backend: session.backend, previous: catalog, signal: controller.signal, session });
426
+ const read = await readProjectToolCatalog(dispatch, { ...scope, backend: session.backend, previous: catalog, signal: controller.signal, session, budgetMs: options.budgetMs });
387
427
  await write(read.catalog);
388
- state.unavailable = read.unavailable;
389
- state.retained = read.retained;
390
428
  state.error = null;
391
429
  adopt(read.catalog, scope ?? { backend: read.backend });
392
430
  return true;
@@ -400,7 +438,12 @@ function createProjectToolCatalog(options) {
400
438
  state.error = { code: error instanceof models_1.ToolingError ? error.code : 'INTERNAL_ERROR', message: error instanceof models_1.ToolingError ? error.message : 'The saved project MCP tools could not be refreshed.' };
401
439
  return false;
402
440
  }
403
- })().finally(() => { running = null; current = null; });
441
+ })().finally(() => {
442
+ // A read the budget left behind must not keep a socket or the dispatcher alive.
443
+ controller.abort();
444
+ running = null;
445
+ current = null;
446
+ });
404
447
  return running;
405
448
  },
406
449
  /** A host process is ending: stop its refresh and free the shared lock for the next one at once. */
@@ -425,7 +468,8 @@ function createProjectToolCatalog(options) {
425
468
  void this.refresh(true);
426
469
  return;
427
470
  }
428
- const connections = catalog.connections.map((item, i) => i === index ? { ...item, tools: normalizeTools(tools) } : item);
471
+ // A catalog just read is a successful read: the connection is available again.
472
+ const connections = catalog.connections.map((item, i) => i === index ? { ...item, tools: normalizeTools(tools), unavailable: undefined } : item);
429
473
  const next = { version: 1, updatedAt: new Date().toISOString(), connections };
430
474
  try {
431
475
  await write(next);