@zackbart/connecta 0.16.0 → 0.17.0

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 (51) hide show
  1. package/AGENTS.md +12 -5
  2. package/CHANGELOG.md +289 -0
  3. package/README.md +6 -1
  4. package/dist/catalog-service.d.ts +4 -0
  5. package/dist/catalog-service.js +49 -5
  6. package/dist/catalog.d.ts +11 -0
  7. package/dist/catalog.js +134 -12
  8. package/dist/errors.d.ts +28 -2
  9. package/dist/errors.js +1 -0
  10. package/dist/execute.d.ts +5 -0
  11. package/dist/execute.js +229 -161
  12. package/dist/invocation.js +3 -1
  13. package/dist/meta-tools.d.ts +4 -0
  14. package/dist/meta-tools.js +46 -14
  15. package/dist/operator-ui/generated.d.ts +1 -1
  16. package/dist/operator-ui/generated.js +1 -1
  17. package/dist/operator-ui/model.d.ts +3 -1
  18. package/dist/providers/cloudflare.js +13 -25
  19. package/dist/providers/mixpanel.d.ts +3 -5
  20. package/dist/providers/mixpanel.js +73 -5
  21. package/dist/providers/stripe.d.ts +2 -2
  22. package/dist/providers/stripe.js +13 -11
  23. package/dist/registry.d.ts +32 -9
  24. package/dist/registry.js +217 -33
  25. package/dist/routes/mcp.js +6 -0
  26. package/dist/skills.d.ts +4 -0
  27. package/dist/skills.js +157 -18
  28. package/dist/types.d.ts +14 -2
  29. package/dist/ui.js +4 -1
  30. package/dist/version.d.ts +1 -1
  31. package/dist/version.js +1 -1
  32. package/documentation/architecture.md +7 -4
  33. package/documentation/cloudflare.md +40 -8
  34. package/documentation/code-first-exploration.md +2 -2
  35. package/documentation/code-mode.md +45 -53
  36. package/documentation/connector-guides.md +24 -19
  37. package/documentation/connectors.md +13 -1
  38. package/documentation/meta-tools.md +33 -18
  39. package/documentation/mixpanel.md +20 -0
  40. package/documentation/notion.md +7 -2
  41. package/documentation/operations.md +74 -29
  42. package/documentation/operator-ui.md +12 -2
  43. package/documentation/provider-audit.md +4 -4
  44. package/documentation/provider-conventions.md +68 -19
  45. package/documentation/stripe.md +45 -14
  46. package/documentation/upgrading.md +478 -0
  47. package/ethos.md +4 -4
  48. package/examples/worker/README.md +13 -6
  49. package/package.json +7 -2
  50. package/templates/node/AGENTS.md +5 -0
  51. package/templates/node/package.json +1 -1
package/AGENTS.md CHANGED
@@ -83,11 +83,18 @@ Two boundaries CI enforces that are not obvious from reading a file:
83
83
  and fails otherwise. Need a Node API? It goes behind an explicit Node-only
84
84
  subpath (`/node` or `/quickjs`), never the root.
85
85
  - **The published surface.** Platform-specific storage adapters live in
86
- `examples/`, not the package. `@clerk/backend` and `quickjs-emscripten` are
87
- optional peers behind the `./auth/clerk` and `./quickjs` subpaths and must
88
- never become dependencies or install with core. Enforced by
89
- `test/package-surface.test.ts` and `scripts/check-package.mjs`. Anything
90
- heavyweight or platform-bound gets a subpath and an optional peer.
86
+ `examples/worker/`, never in `src/` and never in the `exports` map. That
87
+ example does ship in the tarball, Cloudflare KV and D1 adapters included
88
+ it is the Workers starting template a consumer copies, and copying it is the
89
+ point — but every `exports` target resolves into `dist/`, so those adapters
90
+ are readable reference source and not an importable subpath. What is
91
+ forbidden is a platform-bound adapter becoming importable from the package,
92
+ not a file appearing in the artifact. `@clerk/backend` and
93
+ `quickjs-emscripten` are optional peers behind the `./auth/clerk` and
94
+ `./quickjs` subpaths and must never become dependencies or install with
95
+ core. Enforced by `test/package-surface.test.ts` and
96
+ `scripts/check-package.mjs`. Anything heavyweight or platform-bound gets a
97
+ subpath and an optional peer.
91
98
 
92
99
  ## Where new tests go
93
100
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,295 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.17.0 — 2026-08-13
6
+
7
+ This minor release makes catalog discovery faster and its answers more exact.
8
+ Agent reads can use a verified stale catalog while one bounded refresh runs,
9
+ guide summaries now read Markdown as prose, compact schemas retain more declared
10
+ constraints, array projection distinguishes misses from genuine nulls, and
11
+ Mixpanel carries the conditional rules its live tools enforce. One construction
12
+ contract tightens: an explicit `usageGuide.summary` over 120 characters now
13
+ refuses to boot. Deployments whose summaries fit, or which let Connecta derive
14
+ them, need no configuration change. Operator catalog reads retain their blocking
15
+ freshness behavior. This release also makes the two code sandboxes' runtime
16
+ differences explicit. The shipped
17
+ Worker example is already loader-only; deployments that added executor
18
+ `bindings`, `modules`, or `globalOutbound` must remove them. Existing portable
19
+ `execute_code` programs keep their behavior. Programs can now classify a caught
20
+ Connecta failure without parsing its message.
21
+
22
+ ### Changed
23
+
24
+ - An explicit `usageGuide.summary` longer than 120 characters after whitespace
25
+ normalization now throws during registry construction. Exactly 120 remains
26
+ valid, and a blank explicit summary still falls back to derivation (#392).
27
+
28
+ - **Agent catalog reads now serve a verified stale entry while they refresh it.**
29
+ Search, describe, and code-mode calls no longer await a downstream listing
30
+ when the runtime already holds a complete catalog inside its stale window.
31
+ The inbound request still causes the refresh; there is no timer, warmup, or
32
+ credential probe. One bounded refresh per connector owns and closes a fresh
33
+ scope, while operator status stays blocking and shows whether the last agent
34
+ read in this runtime was fresh or stale (#396).
35
+
36
+ ### Fixed
37
+
38
+ - **Caught `execute_code` failures now keep their machine-readable type.**
39
+ Calls, connector shortcuts, discovery, emitted-output and UI validation,
40
+ batch validation, and host-call budgets still throw with the same human
41
+ message, but now expose `code`, `retryable`, and full `details`. Batch entries
42
+ use the same codes. A per-run authenticated frame prevents connector prose or
43
+ guest code from forging the host transport on either executor (#393).
44
+
45
+ - **`execute_code` now tells the truth about each shipped sandbox.** QuickJS
46
+ has no `fetch`, `process`, timers, `crypto`, or `WebSocket`, and blocks
47
+ imports. Loader-only Dynamic Workers deny outbound fetch, WebSocket,
48
+ `node:net`, and `node:tls`; leave DNS unresolved; expose no environment
49
+ bindings or filesystem/HTTP builtins; but retain local `data:` fetch,
50
+ runtime globals, and a non-contract builtin set that can drift. The example
51
+ pins the required loader-only construction, and agent guidance tells portable
52
+ programs to use none of that Dynamic-only authority (#390).
53
+
54
+ - **Clerk-authenticated operator pages now wait for ClerkJS before booting.**
55
+ The loader runs before the later inline operator bundle instead of deferring
56
+ until after parsing, so a fresh page no longer mistakes normal script order
57
+ for a network failure. Clerk's major-to-pinned version redirect remains
58
+ supported, and a real loader failure keeps the existing clear error (#403).
59
+
60
+ - **Stripe's guide now treats connector identity as routing intent, not account
61
+ proof.** One OAuth session may cover several accounts in one organization,
62
+ so agents resolve the intended account through the live tool schema and stop
63
+ when the target or selector is ambiguous. The guide also keeps organization
64
+ accounts separate from the restricted-key-only Stripe Connect path (#404).
65
+
66
+ - **The no-account-model constitution now matches provider-owned sessions.**
67
+ Connecta still has no account dimension: credentials, storage, admission,
68
+ and health remain connector-scoped. A provider may expose its own account
69
+ scope only through its live schema; metadata never proves identity, and an
70
+ ambiguous target or selector stops instead of becoming a guess (#410).
71
+
72
+ - **The reviewed Notion page contracts are current again.** Notion added
73
+ create-page template and placement options plus update-page locking,
74
+ template, and erase options. The existing parent, properties, Markdown,
75
+ children, emoji, and trash request subsets remain valid, so this release
76
+ records the two changed endpoint digests without adding the new capabilities.
77
+ Their product decisions remain in #408 and #409.
78
+
79
+ - **Array field misses now report what happened.** A path that misses every
80
+ element appears in `unmatchedFields` instead of returning a clean array of
81
+ false nulls. A heterogeneous array keeps its positional result and names the
82
+ path in `partialFields`, so genuine downstream nulls remain distinguishable.
83
+ Schema-backed misses keep the same bounded guidance through nested arrays;
84
+ schema-free projections still report their observed misses (#394).
85
+ - Derived guide summaries now join a hard-wrapped opening paragraph before
86
+ selecting a complete sentence or shortening at a clause or word boundary.
87
+ Frontmatter, fences, headings, rules, tables, and description fallbacks keep
88
+ their prior roles; multi-line HTML comments are now skipped whole (#392).
89
+
90
+ - **Compact schemas now carry declared numeric and string constraints.** Search
91
+ and compact describe show numeric bounds, multiples, string length bounds,
92
+ patterns, and formats beside the affected type. Search keeps its 1,024-byte
93
+ schema ceiling and 256-byte node budget: a constraint that does not fit is
94
+ dropped whole, and the existing truncation flag sends the caller to describe
95
+ for the complete shape (#391).
96
+
97
+ - **Carry Mixpanel's three enforced conditional-input rules in its maintained
98
+ guide.** A live read-only audit confirmed that `Get-Business-Context`,
99
+ `Get-Property-Values`, and `List-Properties` accept shapes in their advertised
100
+ schemas that their implementations reject. Connecta still preserves the
101
+ hosted schemas unchanged; the guide now prevents those rejected calls, the
102
+ vetted manifest records schema digests for all 63 tools, and the provider
103
+ defect is tracked upstream. The maintainer drift check also frames
104
+ Mixpanel's service account as its documented `Bearer Basic` value instead of
105
+ ordinary HTTP Basic (#395).
106
+
107
+ ## 0.16.1 — 2026-08-13
108
+
109
+ This is the cleanup that follows 0.16.0 out the door: the packaging housekeeping
110
+ the pre-release smoke gauntlet turned up, one provider tool Cloudflare
111
+ deprecated out from under us, a discovery answer that told a plain lie, and the
112
+ upgrade runbook an existing deployment never had. Two things break, both on
113
+ Cloudflare and both named here rather than left to the section below:
114
+ `list_zone_settings` is gone from the `cloudflare()` named surface, and
115
+ Cloudflare's 404 arrives as `not_found` instead of `connector_call_failed`.
116
+ Nothing else does — no wire shape changes, no construction contract moves, no
117
+ other code reclassified, and the per-setting operations `list_zone_settings`
118
+ sat beside are the supported ones and are untouched. A deployment that writes
119
+ no `api()` connectors, branches on no error code, and never asked an agent for
120
+ a whole zone's settings in one call upgrades without reading further.
121
+
122
+ Three things are additions rather than repairs, and they are the reason this
123
+ release is worth reading rather than just installing: a new
124
+ `ConnectorCallErrorCode` member, `not_found`, with a rule for when a connector
125
+ may mint it; a `"./package.json"` entry in the `exports` map, so the installed
126
+ manifest resolves; and `@cloudflare/codemode` declared as an optional peer at
127
+ `^0.4.4 || ^0.5.0`. Strict semver would read those three as a minor, and would
128
+ read the two Cloudflare changes above as more than that. This ships as a patch
129
+ deliberately: every addition is opt-in at the point a deployment chooses to
130
+ read it, and the tool removal and the 404 reclassification ride along in the
131
+ same patch on purpose — both are scoped to one provider, both have a stated
132
+ replacement, and both carry a version boundary in
133
+ [`documentation/upgrading.md`](./documentation/upgrading.md). Holding them for
134
+ a minor would mean shipping a release that keeps calling an endpoint its
135
+ provider deprecated. The one install-time consequence is spelled out next.
136
+
137
+ One thing to check before upgrading a Worker: `@cloudflare/codemode` is now a
138
+ declared peer, so if your `package.json` holds it at a version outside
139
+ `^0.4.4 || ^0.5.0` — a `0.3.x`, or a `0.4` below `0.4.4` — npm stops the
140
+ upgrade with an `ERESOLVE` conflict rather than installing. Move it into the
141
+ range this release is tested against, or pass `--legacy-peer-deps` if you have
142
+ a reason to run outside it. A version already inside the range, and a range
143
+ loose enough for npm to pick one that is, both resolve exactly as before.
144
+
145
+ Alongside it, the upgrade path an existing deployment takes gets written down.
146
+ `connecta init` was the golden path for a new deployment and the whole story
147
+ for an old one, which is a gap with a shape: `init` refuses to merge into an
148
+ existing path — the guard that keeps an initializer from eating a connector
149
+ set — so an agent pointed at a deployment two releases behind had to
150
+ reconstruct the procedure from release prose written for the maintainer. Both
151
+ interesting failures there were silent too. It overwrites the configuration the
152
+ deployment exists for, or it "fixes" a construction throw by weakening a
153
+ fail-closed default and ships something quieter and wrong.
154
+
155
+ ### Added
156
+
157
+ - **`not_found`, for a downstream that answered and had nothing to give.** A
158
+ hand-written connector meeting a 404 had exactly one honest code,
159
+ `connector_call_failed`, which also means "the call blew up" — so a program
160
+ inside `execute_code` could not tell a clean absence from a broken connector,
161
+ and a loop over ids had to abort where it should have skipped one. The new
162
+ code earns its place the way every code has to: it changes what the caller
163
+ does next. You do not wait, you do not go to `authorize_connector`, you do
164
+ not repair the arguments — you re-address. It is non-retryable, carries no
165
+ recovery envelope, derives no activity friction class, and is exported from
166
+ the root entry as part of `ConnectorCallErrorCode`.
167
+
168
+ The qualifier is the interesting half, and it is now written down in
169
+ [H11](./documentation/provider-conventions.md#h11--errors-are-mapped-to-what-the-caller-does-next):
170
+ map a status to `not_found` only where the provider tells absence apart from
171
+ a permission gap. Cloudflare does — a token that may not touch a resource is
172
+ refused with 401 or 403 — so its 404 is now `not_found` instead of
173
+ `connector_call_failed`. Notion does not: `object_not_found` means both "it
174
+ is gone" and "it was never shared with this integration", so it deliberately
175
+ stays generic with a message that says so. The hosted-MCP proxy path mints
176
+ the code never, because `P1` forbids re-shaping downstream framing and
177
+ provider prose is never parsed to invent a classification (#373).
178
+
179
+ - **An upgrade runbook for existing deployments.**
180
+ [`documentation/upgrading.md`](./documentation/upgrading.md) is written for
181
+ the agent sitting inside a generated deployment it did not create: read the
182
+ exact pin and the template generation it implies, regenerate that generation
183
+ with `npx @zackbart/connecta@<pin> init` to get a real merge base, three-way
184
+ reconcile the scaffolding against the current template while `src/index.ts`
185
+ stays the deployment's own, cross the version boundaries that break
186
+ construction, and finish where `init` finishes — typecheck, start,
187
+ `connecta doctor`, then a program that exercises the deployment's *own*
188
+ connectors, which doctor deliberately knows nothing about. The migration
189
+ notes are per boundary and derived from this file: the 0.16.0 `api()`
190
+ construction contract (with the one safe answer for an unannotated tool
191
+ written down — `readOnlyHint: false`, which is the routing it already had),
192
+ the `linear()`, `mixpanel()`, and Cloudflare provider changes, redirect
193
+ refusal and the response ceilings, and the fail-closed shipped defaults; then
194
+ 0.14's annotation-precedence change, 0.13's rewritten guide summaries, the
195
+ 0.11.0 executor requirement, 0.7.0's `verifyState` requirement and
196
+ core-owned routes for the pre-template deployments that still have to cross
197
+ them, and every removed option that throws with its migration. It closes with five refusals, because each is somebody's plausible
198
+ shortcut: no re-init over the top, no weakening a fail-closed default to get
199
+ green, no pinning back, no vendored internals, no second project shape.
200
+ Reachable from the README, from `operations.md`, and — absolutely, because
201
+ that reader has no copy of this repository — from the template's `AGENTS.md`
202
+ (#380).
203
+ - **A suite that keeps the guide honest.** `test/upgrade-guide.test.ts` pins
204
+ every claim its reader cannot check: the generated file inventory against
205
+ `templates/node/`, the seven tool names against the CLI's own list, each
206
+ named version boundary against a release that shipped, each removed option
207
+ against the release section that names its issue, the bump target against
208
+ this package's version, and the three places the guide is linked from. A template that gains a file now fails `npm run check` rather than
209
+ leaving an agent to guess which of the two is wrong (#380).
210
+ - **`@cloudflare/codemode` is a declared optional peer.** Every Workers
211
+ deployment installs the executor behind `execute_code` by hand, and until now
212
+ the only version range anywhere was a devDependency no consumer can read — a
213
+ fresh install resolved a minor ahead of what this repository tests, silently.
214
+ The manifest now publishes `^0.4.4 || ^0.5.0` for it, optional like
215
+ `@clerk/backend` and `quickjs-emscripten`, so a supported version installs in
216
+ silence and an unsupported one stops the install with something to act on
217
+ instead of becoming skew a Worker discovers in production. It still installs
218
+ with nothing: a default `npm install @zackbart/connecta` pulls no executor,
219
+ and the package smoke proves that, both halves of the range behavior, and
220
+ that the version this repository develops against stays inside the range it
221
+ publishes (#376).
222
+
223
+ ### Changed
224
+
225
+ - **Cloudflare's 404 is `not_found`.** A deployment branching on
226
+ `connector_call_failed` to detect an unknown zone or account id should read
227
+ `not_found` instead; retryability, the message, and its pointer to
228
+ `list_zones` / `list_accounts` are unchanged (#373).
229
+ - `cloudflare()` no longer names a bulk zone-settings read.
230
+ `GET /zones/{zoneId}/settings` and its `PATCH` sibling are published as
231
+ `deprecated: true`, Cloudflare offers no bulk replacement, and the tool that
232
+ wrapped the read projected nothing — it took a zone id and grew the payload
233
+ by wrapping an unpaginated array in a page object. Read one setting with
234
+ `get_zone_setting` and write one with `update_zone_setting`, both on the
235
+ supported `/zones/{zoneId}/settings/{settingId}` operations. An operator who
236
+ still wants the whole set can name the deprecated path explicitly through
237
+ `cloudflare_api_get`. The named surface is 47 tools plus the three escape
238
+ hatches ([#361](https://github.com/zackbart/connecta/issues/361)).
239
+ - The Cloudflare touched-endpoint manifest drops the deprecated row with the
240
+ tool, so `npm run drift:check -- --specs` is quiet about zone settings
241
+ because nothing calls the endpoint, not because a maintainer signed off on
242
+ calling it anyway.
243
+
244
+ ### Fixed
245
+
246
+ - **A search for a connector's own name stops claiming the deployment has no
247
+ such capability.** A connector's `id` — the address prefix an agent already
248
+ holds — and its `title` are displayed, never indexed, so `search_tools({
249
+ query: "inventory" })` against a connector called `inventory` matched no tool
250
+ and was answered with "No matching capability is configured in this
251
+ deployment", which was plainly false. Connector identity stays out of the
252
+ lexical index, because putting it in would move ranking for every query that
253
+ already matches tools; instead an unscoped miss whose terms name configured
254
+ connectors says so, names up to three of them by ID, and sends the caller to
255
+ a scoped browse. A term that matches nothing in the deployment still gets the
256
+ original sentence, unchanged. One `queryAnalysis.guidance` string differs; no
257
+ ranking, result, or field changed (#372).
258
+ - **Every relative link in the shipped Markdown resolves for the reader who
259
+ installed the package.** Ten of them pointed at `eval/`, `test/`, `scripts/`,
260
+ and the README hero — repository paths the tarball has never carried and, per
261
+ #346, should not start carrying. The link gate could not see any of them: it
262
+ read only `documentation/` targets, so the whole class was invisible and grew
263
+ with every trim. The policy is now stated once in the operations guide and
264
+ enforced over *every* relative link in packed Markdown: it either resolves
265
+ inside the tarball or it is cited as an absolute
266
+ `https://github.com/zackbart/connecta/blob/main/...` URL, which an outside
267
+ reader can follow and which `check:docs` resolves back to the checkout, so a
268
+ citation still fails when the file it names moves. The ten links were
269
+ rewritten that way, the README hero now loads from
270
+ `raw.githubusercontent.com` and still renders on npmjs.com, and the
271
+ repository reader loses no citation (#378).
272
+ - **`@zackbart/connecta/package.json` resolves.** The `exports` map listed
273
+ every code subpath and nothing else, so a bundler plugin, framework build
274
+ step, or version probe reaching for the installed manifest — a thing the
275
+ ecosystem broadly expects to work — got `ERR_PACKAGE_PATH_NOT_EXPORTED`
276
+ instead of the file. The manifest is now exported. It is a data file, so this
277
+ widens the published surface by exactly zero code paths: the root entry's
278
+ Workers purity boundary and the optional-peer subpaths are untouched. The
279
+ package-surface gate now asserts the whole subpath set, manifest included, so
280
+ neither this entry nor an unwanted one can arrive unnoticed (#374).
281
+ - **The published-surface rule says what it actually forbids.** `AGENTS.md`
282
+ claimed platform-specific storage adapters live in `examples/`, "not the
283
+ package", while the tarball has always carried `examples/worker` — Cloudflare
284
+ KV and D1 adapters included — because that example is the Workers starting
285
+ template a consumer copies. The invariant was never in danger: nothing under
286
+ `examples/` appears in the `exports` map, so those adapters are reference
287
+ source and not an importable subpath. The wording now draws the line where
288
+ the gates draw it — a platform-bound adapter must not reach `src/` or the
289
+ `exports` map — and says why the example ships, in `AGENTS.md`, the
290
+ operations guide, and the `scripts/check-package.mjs` comment. A new
291
+ assertion in `test/package-surface.test.ts` holds the instruction file and
292
+ the exports map to the same story (#377).
293
+
5
294
  ## 0.16.0 — 2026-08-12
6
295
 
7
296
  This is the agent-efficiency refocus. One release, sixteen merges, and a single
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # connecta
2
2
 
3
- ![A monochrome clay Connecta hub joining many tools](./assets/connecta-clay-hero.png)
3
+ ![A monochrome clay Connecta hub joining many tools](https://raw.githubusercontent.com/zackbart/connecta/main/assets/connecta-clay-hero.png)
4
4
 
5
5
  One place for AI agents to connect to the tools you choose.
6
6
 
@@ -98,6 +98,11 @@ activity ship as commented configuration, each one a variable and an
98
98
  uncommented block away. The generated `README.md` walks through all four, and
99
99
  the [Worker example](./examples/worker/) does the same for KV and D1.
100
100
 
101
+ Already have a deployment on an older version? `init` deliberately refuses to
102
+ merge into it, so bringing one current is its own procedure:
103
+ [Upgrading an existing deployment](./documentation/upgrading.md) is the
104
+ runbook, written for the agent working inside that project.
105
+
101
106
  The template refuses to merge into an existing directory, so initialization
102
107
  cannot overwrite another project. Its generated programs have no filesystem,
103
108
  environment, arbitrary network, imports, or timers; only explicitly read-only
@@ -1,5 +1,6 @@
1
1
  import type { CallErrorDetails } from "./errors.js";
2
2
  import type { ConnectorOperationOptions, RegistryView } from "./registry.js";
3
+ import type { DeferredWork } from "./connector-scope.js";
3
4
  import type { Connector, ToolDef } from "./types.js";
4
5
  export declare const DEFAULT_SEARCH_LIMIT = 8;
5
6
  export declare const MAX_SEARCH_LIMIT = 100;
@@ -138,6 +139,7 @@ export declare class CatalogService {
138
139
  private readonly probeTimeoutMs;
139
140
  private readonly concurrency;
140
141
  private readonly searchRoute;
142
+ private readonly readOptions;
141
143
  private readonly loaded;
142
144
  private readonly loading;
143
145
  constructor(registry: RegistryView, baseUrl: string, options?: {
@@ -146,6 +148,8 @@ export declare class CatalogService {
146
148
  concurrency?: number;
147
149
  /** The discovery route recovery records name. Default `search_tools`. */
148
150
  searchRoute?: SearchRoute;
151
+ /** Runtime-owned tail for stale-while-revalidate catalog reads. */
152
+ defer?: DeferredWork;
149
153
  });
150
154
  /**
151
155
  * Send a caller back to discovery through the surface it can actually reach.
@@ -1,4 +1,4 @@
1
- import { compactDiscoverySchema, compactSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, } from "./catalog.js";
1
+ import { compactDiscoverySchema, compactSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, matchesLexicalTerm, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, } from "./catalog.js";
2
2
  import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concurrency.js";
3
3
  import { boundedEchoText, classifyCallError, framingError, } from "./errors.js";
4
4
  import { connectorGuide, connectorGuideRequired, connectorGuideSummary, connectorSkillName, } from "./skills.js";
@@ -10,6 +10,12 @@ export const MAX_DESCRIBE_ADDRESSES = 100;
10
10
  export const MAX_DISCOVERY_RESULT_BYTES = 256_000;
11
11
  const MAX_QUERY_TERMS = 8;
12
12
  const MAX_QUERY_TERM_LENGTH = 64;
13
+ /**
14
+ * Connector IDs a no-match search will name back. Three is enough to point at
15
+ * the connector the query already named without turning a miss into a listing
16
+ * of the deployment.
17
+ */
18
+ const MAX_IDENTITY_CONNECTORS = 3;
13
19
  const encoder = new TextEncoder();
14
20
  /** Clip one echoed query term without splitting a non-BMP code point. */
15
21
  function boundedQueryTerm(term) {
@@ -161,6 +167,7 @@ export class CatalogService {
161
167
  probeTimeoutMs;
162
168
  concurrency;
163
169
  searchRoute;
170
+ readOptions;
164
171
  loaded = new Map();
165
172
  loading = new Map();
166
173
  constructor(registry, baseUrl, options = {}) {
@@ -171,6 +178,12 @@ export class CatalogService {
171
178
  normalizeTimeoutMs(options.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
172
179
  this.concurrency = resolveDiscoveryConcurrency(options.concurrency);
173
180
  this.searchRoute = options.searchRoute ?? "search_tools";
181
+ this.readOptions = options.defer
182
+ ? {
183
+ defer: options.defer,
184
+ refreshTimeoutMs: this.probeTimeoutMs,
185
+ }
186
+ : undefined;
174
187
  }
175
188
  /**
176
189
  * Send a caller back to discovery through the surface it can actually reach.
@@ -201,7 +214,7 @@ export class CatalogService {
201
214
  if (inFlight)
202
215
  return inFlight;
203
216
  const loading = this.registry
204
- .getTools(id, this.baseUrl, this.requestScope, callOptions)
217
+ .getTools(id, this.baseUrl, this.requestScope, callOptions, this.readOptions)
205
218
  .then((tools) => {
206
219
  this.loaded.set(id, tools);
207
220
  return tools;
@@ -561,6 +574,36 @@ export class CatalogService {
561
574
  required: connectorGuideRequired(scopedConnector),
562
575
  }
563
576
  : undefined;
577
+ // A connector's own id — the address prefix the caller already has — and
578
+ // the title it is displayed under are the most natural first query terms,
579
+ // and neither is a document in the lexical index. Indexing them would move
580
+ // ranking for every query that already matches tools, so instead a search
581
+ // that matched nothing asks the same lexical question of connector
582
+ // identity and corrects its own sentence: the deployment plainly has this
583
+ // capability, and one scoped browse away are its tools. Unscoped only —
584
+ // guidance for an explicit scope already names that connector rather than
585
+ // claiming the deployment has nothing.
586
+ const identityConnectorIds = matches.length === 0 && !isBrowse && !unsearchableQuery && !scopedConnector
587
+ ? connectors
588
+ // The full query, not `analyzedTerms`: that cap exists to bound
589
+ // the serialized term fields, and ranking already reads every
590
+ // term. A ninth term is a real search term, and a search that
591
+ // ranked against it must not deny the connector it names.
592
+ .filter((connector) => queryTerms.some((term) => matchesLexicalTerm(connector.id, term) ||
593
+ (connector.title !== undefined &&
594
+ matchesLexicalTerm(connector.title, term))))
595
+ .map((connector) => connector.id)
596
+ : [];
597
+ const namedIdentityConnectors = identityConnectorIds
598
+ .slice(0, MAX_IDENTITY_CONNECTORS)
599
+ .map((id) => `"${id}"`)
600
+ .join(", ");
601
+ const unnamedIdentityConnectors = identityConnectorIds.length - MAX_IDENTITY_CONNECTORS;
602
+ const identityGuidance = identityConnectorIds.length === 0
603
+ ? undefined
604
+ : `No matching ${safetyLabel}capability was found${unavailableCatalogs === 0 ? "" : " in the catalogs that answered"}, but the query names configured connector${identityConnectorIds.length === 1 ? "" : "s"} ${namedIdentityConnectors}${unnamedIdentityConnectors > 0
605
+ ? ` and ${unnamedIdentityConnectors} more`
606
+ : ""}. Scope by connector and browse with an empty query to list the tools there.${filterRecovery}`;
564
607
  // A scope that resolved to nothing is the same silence one step earlier in
565
608
  // the lookup: no connector resolved, so no catalog was even attempted, so
566
609
  // no catalog failed and the unavailable path below never fires. Echo only
@@ -604,9 +647,10 @@ export class CatalogService {
604
647
  : scopedGuide?.required
605
648
  ? `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Fetch queryAnalysis.guide before calling, then refine terms or browse with an empty query.${filterRecovery}`
606
649
  : `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Refine terms or browse it with an empty query.${filterRecovery}`
607
- : unavailableCatalogs === 0
608
- ? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
609
- : `No matching ${safetyLabel}capability was found in the catalogs that answered; ${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`))
650
+ : (identityGuidance ??
651
+ (unavailableCatalogs === 0
652
+ ? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
653
+ : `No matching ${safetyLabel}capability was found in the catalogs that answered; ${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`))))
610
654
  : matchMode === "partial"
611
655
  ? scopedConnector
612
656
  ? `No single tool on connector "${scopedConnector.id}" matched every term. Split distinct intents into separate searches.`
package/dist/catalog.d.ts CHANGED
@@ -18,6 +18,17 @@ export interface RankedTool {
18
18
  exactName: boolean;
19
19
  matchedTermCount: number;
20
20
  }
21
+ /**
22
+ * Whether one query term matches a whole token of arbitrary text, under the
23
+ * same inflection rules the tool index uses.
24
+ *
25
+ * Connector identity — an `id` or a `title` — is deliberately not a document in
26
+ * that index: making it one would move ranking for every query that already
27
+ * matches tools. This lets a caller ask the index's question of a string that
28
+ * never became a document, which is what the no-match analysis needs to tell
29
+ * "nothing like this exists here" from "that word is a connector".
30
+ */
31
+ export declare function matchesLexicalTerm(text: string, term: string): boolean;
21
32
  export interface LexicalCorpusStatistics {
22
33
  documentCount: number;
23
34
  documentFrequency: ReadonlyMap<string, number>;