@zackbart/connecta 0.18.2 → 0.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.18.3 — 2026-08-25
6
+
7
+ This change gives code mode memory where downstream MCP catalogs are usually
8
+ silent: after a successful call with no declared output schema, Connecta keeps
9
+ only the result's field names and broad JSON types and shows that open shape on
10
+ later discovery. Nothing breaks, no deployment option changes, and providers
11
+ that already declare outputs are untouched. The bounded cache lives only in the
12
+ current process or Worker isolate and forgets each shape after 24 hours.
13
+ Discovery still never executes a tool, and
14
+ the new `outputSchemaSource: "observed"` marker is the warning label that keeps
15
+ one or several real results from masquerading as a provider contract.
16
+
17
+ ### Added
18
+
19
+ - **Passive observed output schemas.** Successful explicitly read-only calls
20
+ whose provider declared no `outputSchema` infer and merge an open
21
+ optional-field schema of names and broad JSON types. The inference retains no
22
+ arguments, scalar values, raw results, code, credentials, or errors. Property
23
+ names may be user-authored. It bounds depth, breadth, names, nodes, definition
24
+ size, and schema bytes; keeps at most 256 runtime entries; ties each entry to
25
+ the exact tool definition; and expires it after 24 hours.
26
+ Search and describe return it with `outputSchemaSource: "observed"`; a
27
+ declared schema always wins. Observation is synchronous, storage-free, and
28
+ unable to fail the call. The design follows new warm-cache evidence and two
29
+ live deployment audits: BePresent had 246/378 tools without output schemas,
30
+ while OneMany's maintained connectors declared all 90 (#442).
31
+
5
32
  ## 0.18.2 — 2026-08-18
6
33
 
7
34
  This patch closes the gap the RevenueCat rollout exposed on the same day 0.18.1
package/README.md CHANGED
@@ -67,6 +67,10 @@ Fifty issues in, one small object out. Your context window notices.
67
67
  classifications, imported one at a time.
68
68
  - **Let the agent work in code.** Search, chain, filter, join, and reduce
69
69
  inside the sandbox instead of round-tripping every call through the model.
70
+ - **Teach undeclared result shapes by using them.** Successful read-only calls
71
+ retain field names and broad types in bounded runtime memory, never scalar
72
+ values, so later programs can project a remote MCP result its provider never
73
+ documented.
70
74
  - **Keep writes deliberate.** Only tools marked read-only run in a program.
71
75
  Everything else is a separate, visible call your client can gate.
72
76
  - **Run it where you like.** Node, a Docker container, or a Cloudflare Worker,
@@ -49,6 +49,7 @@ interface CatalogSearchEntry {
49
49
  description?: string;
50
50
  inputSchema?: unknown;
51
51
  outputSchema?: unknown;
52
+ outputSchemaSource?: "observed";
52
53
  inputSchemaTruncated?: true;
53
54
  outputSchemaTruncated?: true;
54
55
  inputKeys?: string[];
@@ -112,6 +113,7 @@ export interface CatalogDescription {
112
113
  guideRequiredReasons?: GuideRequiredReason[];
113
114
  inputSchema?: unknown;
114
115
  outputSchema?: unknown;
116
+ outputSchemaSource?: "observed";
115
117
  annotations?: ToolDef["annotations"];
116
118
  error?: string;
117
119
  errorDetails?: CatalogDescriptionFailureDetail;
@@ -174,6 +176,7 @@ export declare class CatalogService {
174
176
  }, purpose: string): NonNullable<CallErrorDetails["nextAction"]>;
175
177
  loadConnector(id: string, callOptions?: ConnectorOperationOptions): Promise<ToolDef[]>;
176
178
  private loadForDiscovery;
179
+ private outputSchema;
177
180
  resolveTool(address: string, callOptions?: ConnectorOperationOptions): Promise<CatalogResolution>;
178
181
  /**
179
182
  * Resolve the JavaScript-safe property used by a lazy code-mode namespace
@@ -242,6 +245,7 @@ export declare function flatSearchResult(page: CatalogSearchPage): {
242
245
  description?: string;
243
246
  inputSchema?: unknown;
244
247
  outputSchema?: unknown;
248
+ outputSchemaSource?: "observed";
245
249
  inputSchemaTruncated?: true;
246
250
  outputSchemaTruncated?: true;
247
251
  inputKeys?: string[];
@@ -266,6 +266,14 @@ export class CatalogService {
266
266
  timeoutMs: this.probeTimeoutMs,
267
267
  }), this.probeTimeoutMs, label);
268
268
  }
269
+ outputSchema(connectorId, tool) {
270
+ if (tool.outputSchema)
271
+ return { schema: tool.outputSchema };
272
+ const observed = this.registry.observedOutputSchema(connectorId, tool);
273
+ return observed
274
+ ? { schema: observed, source: "observed" }
275
+ : {};
276
+ }
269
277
  async resolveTool(address, callOptions = {}) {
270
278
  const resolved = this.registry.resolveAddress(address);
271
279
  if (!resolved) {
@@ -487,15 +495,18 @@ export class CatalogService {
487
495
  });
488
496
  const pageMatches = matches.slice(offset, offset + limit);
489
497
  const entries = pageMatches.map((match) => {
498
+ const output = args.includeSchemas
499
+ ? this.outputSchema(match.connector.id, match.tool)
500
+ : {};
490
501
  const input = match.tool.inputSchema ?? { type: "object" };
491
502
  const renderedInput = args.includeSchemas
492
503
  ? renderSearchSchema(input, args.includeSchemas)
493
504
  : undefined;
494
- const renderedOutput = args.includeSchemas && match.tool.outputSchema
495
- ? renderSearchSchema(match.tool.outputSchema, args.includeSchemas)
505
+ const renderedOutput = args.includeSchemas && output.schema
506
+ ? renderSearchSchema(output.schema, args.includeSchemas)
496
507
  : undefined;
497
508
  const schemaKeys = args.includeSchemas && args.includeSchemaKeys
498
- ? schemaKeyMetadata(input, match.tool.outputSchema)
509
+ ? schemaKeyMetadata(input, output.schema)
499
510
  : undefined;
500
511
  const description = summarizeDiscoveryDescription(match.tool.description, args.fullDescriptions === true);
501
512
  const requiredReasons = guideRequiredReasons(match.connector, match.tool, renderedInput?.truncated === true || renderedOutput?.truncated === true);
@@ -520,11 +531,14 @@ export class CatalogService {
520
531
  ...(renderedInput?.truncated
521
532
  ? { inputSchemaTruncated: true }
522
533
  : {}),
523
- ...(args.includeSchemas && match.tool.outputSchema
534
+ ...(args.includeSchemas && output.schema
524
535
  ? {
525
536
  outputSchema: renderedOutput?.schema,
526
537
  }
527
538
  : {}),
539
+ ...(args.includeSchemas && output.source
540
+ ? { outputSchemaSource: output.source }
541
+ : {}),
528
542
  ...(renderedOutput?.truncated
529
543
  ? { outputSchemaTruncated: true }
530
544
  : {}),
@@ -813,6 +827,7 @@ export class CatalogService {
813
827
  };
814
828
  }
815
829
  const input = tool.inputSchema ?? { type: "object" };
830
+ const output = this.outputSchema(addressResolution.connector.id, tool);
816
831
  const description = summarizeDescription(tool.description, args.fullDescriptions === true);
817
832
  const requiredReasons = guideRequiredReasons(addressResolution.connector, tool, false);
818
833
  const guideSummary = connectorGuideSummary(addressResolution.connector);
@@ -833,11 +848,12 @@ export class CatalogService {
833
848
  }
834
849
  : {}),
835
850
  inputSchema: renderSchema(input, format),
836
- ...(tool.outputSchema
851
+ ...(output.schema
837
852
  ? {
838
- outputSchema: renderSchema(tool.outputSchema, format),
853
+ outputSchema: renderSchema(output.schema, format),
839
854
  }
840
855
  : {}),
856
+ ...(output.source ? { outputSchemaSource: output.source } : {}),
841
857
  ...(tool.annotations ? { annotations: tool.annotations } : {}),
842
858
  };
843
859
  });
@@ -288,6 +288,7 @@ export class InvocationService {
288
288
  }
289
289
  const maxRetries = Math.min(2, Math.max(0, Math.trunc(context.maxRetries ?? 0)));
290
290
  let result;
291
+ let observedResult;
291
292
  while (true) {
292
293
  attempts++;
293
294
  let permit;
@@ -362,9 +363,8 @@ export class InvocationService {
362
363
  // reports the same downstream-failure wording, and the throw lands
363
364
  // inside the attempt where it stays retry-eligible and feeds health.
364
365
  assertRawMcpSuccess(resolved.connector.kind, raw);
365
- result = context.unwrapResult
366
- ? unwrapMcpResult(resolved.connector.kind, raw)
367
- : raw;
366
+ observedResult = unwrapMcpResult(resolved.connector.kind, raw);
367
+ result = context.unwrapResult ? observedResult : raw;
368
368
  }
369
369
  finally {
370
370
  connectorMs += Date.now() - connectorStarted;
@@ -440,6 +440,12 @@ export class InvocationService {
440
440
  const value = context.processResult
441
441
  ? await context.processResult(result, resolved)
442
442
  : result;
443
+ try {
444
+ this.registry.observeOutputShape(resolved.connector.id, resolved.definition, observedResult);
445
+ }
446
+ catch {
447
+ // Shape learning is advisory. It cannot change a completed call.
448
+ }
443
449
  resultProcessingMs += Date.now() - processingStarted;
444
450
  const diagnostics = timing();
445
451
  const friction = context.activityFriction?.(value);
@@ -113,6 +113,10 @@ export interface RegistryView {
113
113
  observedSuccessAt(id: string): string | undefined;
114
114
  /** Local declared-vs-stored credential mismatch, with no downstream I/O. */
115
115
  credentialDriftFor(id: string): Promise<string | undefined>;
116
+ /** Value-free shape learned from successful calls, never a provider declaration. */
117
+ observedOutputSchema(connectorId: string, definition: ToolDef): ToolDef["outputSchema"] | undefined;
118
+ /** Passively learn one successful unwrapped result; failures stay isolated. */
119
+ observeOutputShape(connectorId: string, definition: ToolDef, value: unknown): void;
116
120
  statusFor(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions): Promise<ConnectorStatus>;
117
121
  invalidateStored(id: string): Promise<void>;
118
122
  }
@@ -141,6 +145,7 @@ export declare class Registry implements RegistryView {
141
145
  private readonly health;
142
146
  /** Last drift counts reported to activity, per connector, in this runtime. */
143
147
  private readonly reportedDrift;
148
+ private readonly observedOutputSchemas;
144
149
  private readonly ttlMs;
145
150
  private readonly staleMs;
146
151
  private readonly persistToolCatalog;
@@ -203,6 +208,8 @@ export declare class Registry implements RegistryView {
203
208
  * separate from any connector's `conn:<id>:` namespace. Backs get_result.
204
209
  */
205
210
  resultsStorage(): KVStorage;
211
+ observedOutputSchema(connectorId: string, definition: ToolDef): ToolDef["outputSchema"] | undefined;
212
+ observeOutputShape(connectorId: string, definition: ToolDef, value: unknown): void;
206
213
  /** Resolve "<connectorId>.<toolName>" → connector + tool name. */
207
214
  resolveAddress(address: string): {
208
215
  connector: Connector;
package/dist/registry.js CHANGED
@@ -7,6 +7,7 @@ import { boundedCatalogDrift } from "./catalog-drift.js";
7
7
  import { fingerprintSerializedCatalog, snapshotCatalog, } from "./catalog-fingerprint.js";
8
8
  import { MAX_CATALOG_CHUNK_BYTES, MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES, } from "./catalog-limits.js";
9
9
  import { mapSettledWithConcurrency } from "./concurrency.js";
10
+ import { ObservedOutputSchemas } from "./result-shapes.js";
10
11
  import { GUIDE_SUMMARY_LENGTH, normalizeGuideSummary, } from "./skills.js";
11
12
  import { withAbortableTimeout } from "./timeout.js";
12
13
  const ID_RE = /^[a-z0-9_-]+$/;
@@ -133,6 +134,7 @@ export class Registry {
133
134
  health = new HealthLog();
134
135
  /** Last drift counts reported to activity, per connector, in this runtime. */
135
136
  reportedDrift = new Map();
137
+ observedOutputSchemas;
136
138
  ttlMs;
137
139
  staleMs;
138
140
  persistToolCatalog;
@@ -140,6 +142,7 @@ export class Registry {
140
142
  maxResultBytes;
141
143
  constructor(connectors, opts) {
142
144
  this.opts = opts;
145
+ this.observedOutputSchemas = new ObservedOutputSchemas();
143
146
  this.ttlMs =
144
147
  (opts.toolCacheTtlSeconds ?? DEFAULT_TTL_SECONDS) * 1000;
145
148
  this.staleMs =
@@ -342,6 +345,12 @@ export class Registry {
342
345
  resultsStorage() {
343
346
  return namespaced(this.opts.storage, "results:");
344
347
  }
348
+ observedOutputSchema(connectorId, definition) {
349
+ return this.observedOutputSchemas.get(connectorId, definition);
350
+ }
351
+ observeOutputShape(connectorId, definition, value) {
352
+ this.observedOutputSchemas.observe(connectorId, definition, value);
353
+ }
345
354
  /** Resolve "<connectorId>.<toolName>" → connector + tool name. */
346
355
  resolveAddress(address) {
347
356
  const parts = splitAddress(address);
@@ -0,0 +1,13 @@
1
+ import type { JsonSchema, ToolDef } from "./types.js";
2
+ /**
3
+ * Passive, process-local output-shape learning. Provider declarations always
4
+ * win; observations are an open, optional-field routing aid and never a
5
+ * replacement contract.
6
+ */
7
+ export declare class ObservedOutputSchemas {
8
+ private readonly entries;
9
+ private cacheKey;
10
+ private set;
11
+ get(connectorId: string, definition: ToolDef): JsonSchema | undefined;
12
+ observe(connectorId: string, definition: ToolDef, value: unknown): void;
13
+ }
@@ -0,0 +1,331 @@
1
+ import { isExplicitlyReadOnly } from "./tool-safety.js";
2
+ const OBSERVATION_TTL_MS = 24 * 60 * 60 * 1000;
3
+ const MAX_CACHED_SHAPES = 256;
4
+ const MAX_DEFINITION_BYTES = 64 * 1024;
5
+ const MAX_SCHEMA_BYTES = 16 * 1024;
6
+ const MAX_SCHEMA_DEPTH = 6;
7
+ const MAX_SCHEMA_NODES = 128;
8
+ const MAX_OBJECT_PROPERTIES = 48;
9
+ const MAX_ARRAY_ITEMS = 32;
10
+ const MAX_PROPERTY_NAME_BYTES = 128;
11
+ const UNSAFE_PROPERTY_NAMES = new Set([
12
+ "__proto__",
13
+ "constructor",
14
+ "prototype",
15
+ ]);
16
+ const encoder = new TextEncoder();
17
+ function broadType(value) {
18
+ if (value === null)
19
+ return "null";
20
+ if (Array.isArray(value))
21
+ return "array";
22
+ switch (typeof value) {
23
+ case "string":
24
+ case "boolean":
25
+ return typeof value;
26
+ case "number":
27
+ return Number.isFinite(value) ? "number" : undefined;
28
+ case "object":
29
+ return "object";
30
+ default:
31
+ return undefined;
32
+ }
33
+ }
34
+ function serializedSchema(schema) {
35
+ try {
36
+ const text = JSON.stringify(schema);
37
+ return encoder.encode(text).byteLength <= MAX_SCHEMA_BYTES
38
+ ? text
39
+ : undefined;
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ function cloneSchema(schema) {
46
+ const serialized = serializedSchema(schema);
47
+ if (!serialized)
48
+ return undefined;
49
+ try {
50
+ return JSON.parse(serialized);
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ function definitionIdentity(definition) {
57
+ try {
58
+ const serialized = JSON.stringify(definition);
59
+ return encoder.encode(serialized).byteLength <= MAX_DEFINITION_BYTES
60
+ ? serialized
61
+ : undefined;
62
+ }
63
+ catch {
64
+ return undefined;
65
+ }
66
+ }
67
+ function schemaType(schema) {
68
+ return typeof schema.type === "string" ? schema.type : undefined;
69
+ }
70
+ function safePropertyName(name) {
71
+ return (!UNSAFE_PROPERTY_NAMES.has(name) &&
72
+ encoder.encode(name).byteLength <= MAX_PROPERTY_NAME_BYTES);
73
+ }
74
+ function boundedPropertyNames(value) {
75
+ const names = [];
76
+ for (const name in value) {
77
+ if (!Object.hasOwn(value, name) || !safePropertyName(name))
78
+ continue;
79
+ names.push(name);
80
+ if (names.length >= MAX_OBJECT_PROPERTIES)
81
+ break;
82
+ }
83
+ return names.sort();
84
+ }
85
+ function unionBranches(schema) {
86
+ return Array.isArray(schema.anyOf)
87
+ ? schema.anyOf
88
+ : [schema];
89
+ }
90
+ function branchOrder(schema) {
91
+ return ["null", "boolean", "number", "string", "array", "object"].indexOf(schemaType(schema) ?? "");
92
+ }
93
+ function mergeSchemas(left, right) {
94
+ const leftType = schemaType(left);
95
+ const rightType = schemaType(right);
96
+ if (leftType === "object" && rightType === "object") {
97
+ const leftProperties = left.properties && typeof left.properties === "object"
98
+ ? left.properties
99
+ : {};
100
+ const rightProperties = right.properties && typeof right.properties === "object"
101
+ ? right.properties
102
+ : {};
103
+ const properties = Object.create(null);
104
+ for (const key of [...new Set([
105
+ ...Object.keys(leftProperties),
106
+ ...Object.keys(rightProperties),
107
+ ])].sort()) {
108
+ const leftProperty = leftProperties[key];
109
+ const rightProperty = rightProperties[key];
110
+ properties[key] =
111
+ leftProperty && rightProperty
112
+ ? mergeSchemas(leftProperty, rightProperty)
113
+ : (leftProperty ?? rightProperty);
114
+ }
115
+ return { type: "object", properties };
116
+ }
117
+ if (leftType === "array" && rightType === "array") {
118
+ const leftItems = left.items && typeof left.items === "object"
119
+ ? left.items
120
+ : undefined;
121
+ const rightItems = right.items && typeof right.items === "object"
122
+ ? right.items
123
+ : undefined;
124
+ return {
125
+ type: "array",
126
+ ...(leftItems || rightItems
127
+ ? {
128
+ items: leftItems && rightItems
129
+ ? mergeSchemas(leftItems, rightItems)
130
+ : (leftItems ?? rightItems),
131
+ }
132
+ : {}),
133
+ };
134
+ }
135
+ if (leftType && leftType === rightType)
136
+ return left;
137
+ const byType = new Map();
138
+ for (const branch of [...unionBranches(left), ...unionBranches(right)]) {
139
+ const type = schemaType(branch);
140
+ if (!type)
141
+ continue;
142
+ const existing = byType.get(type);
143
+ byType.set(type, existing ? mergeSchemas(existing, branch) : branch);
144
+ }
145
+ const branches = [...byType.values()].sort((a, b) => branchOrder(a) - branchOrder(b));
146
+ return branches.length === 1 ? branches[0] : { anyOf: branches };
147
+ }
148
+ function inferSchema(value, budget, depth = 0) {
149
+ const type = broadType(value);
150
+ if (!type || depth > MAX_SCHEMA_DEPTH || budget.nodes >= MAX_SCHEMA_NODES) {
151
+ return undefined;
152
+ }
153
+ budget.nodes++;
154
+ if (type !== "object" && type !== "array")
155
+ return { type };
156
+ const object = value;
157
+ if (budget.seen.has(object))
158
+ return undefined;
159
+ budget.seen.add(object);
160
+ try {
161
+ if (type === "array") {
162
+ let items;
163
+ for (const item of value.slice(0, MAX_ARRAY_ITEMS)) {
164
+ const inferred = inferSchema(item, budget, depth + 1);
165
+ if (inferred)
166
+ items = items ? mergeSchemas(items, inferred) : inferred;
167
+ if (budget.nodes >= MAX_SCHEMA_NODES)
168
+ break;
169
+ }
170
+ return { type: "array", ...(items ? { items } : {}) };
171
+ }
172
+ const properties = Object.create(null);
173
+ const record = value;
174
+ for (const key of boundedPropertyNames(record)) {
175
+ const inferred = inferSchema(record[key], budget, depth + 1);
176
+ if (inferred)
177
+ properties[key] = inferred;
178
+ if (budget.nodes >= MAX_SCHEMA_NODES)
179
+ break;
180
+ }
181
+ return { type: "object", properties };
182
+ }
183
+ finally {
184
+ budget.seen.delete(object);
185
+ }
186
+ }
187
+ function boundSchema(schema, budget, depth = 0) {
188
+ if (depth > MAX_SCHEMA_DEPTH || budget.nodes >= MAX_SCHEMA_NODES) {
189
+ return undefined;
190
+ }
191
+ budget.nodes++;
192
+ if (Array.isArray(schema.anyOf)) {
193
+ const branches = schema.anyOf
194
+ .slice(0, 6)
195
+ .map((branch) => boundSchema(branch, budget, depth + 1))
196
+ .filter((branch) => Boolean(branch));
197
+ return branches.length >= 2 ? { anyOf: branches } : branches[0];
198
+ }
199
+ const type = schemaType(schema);
200
+ if (!type)
201
+ return undefined;
202
+ if (type === "array") {
203
+ const items = schema.items && typeof schema.items === "object"
204
+ ? boundSchema(schema.items, budget, depth + 1)
205
+ : undefined;
206
+ return { type: "array", ...(items ? { items } : {}) };
207
+ }
208
+ if (type === "object") {
209
+ const source = schema.properties && typeof schema.properties === "object"
210
+ ? schema.properties
211
+ : {};
212
+ const properties = Object.create(null);
213
+ let included = 0;
214
+ for (const key of Object.keys(source).sort()) {
215
+ if (included >= MAX_OBJECT_PROPERTIES ||
216
+ !safePropertyName(key)) {
217
+ continue;
218
+ }
219
+ const property = boundSchema(source[key], budget, depth + 1);
220
+ if (property) {
221
+ properties[key] = property;
222
+ included++;
223
+ }
224
+ if (budget.nodes >= MAX_SCHEMA_NODES)
225
+ break;
226
+ }
227
+ return { type: "object", properties };
228
+ }
229
+ return { type };
230
+ }
231
+ function observedSchema(value) {
232
+ try {
233
+ const inferred = inferSchema(value, { nodes: 0, seen: new WeakSet() });
234
+ const schema = inferred
235
+ ? boundSchema(inferred, { nodes: 0 })
236
+ : undefined;
237
+ return schema && serializedSchema(schema) ? schema : undefined;
238
+ }
239
+ catch {
240
+ return undefined;
241
+ }
242
+ }
243
+ /**
244
+ * Passive, process-local output-shape learning. Provider declarations always
245
+ * win; observations are an open, optional-field routing aid and never a
246
+ * replacement contract.
247
+ */
248
+ export class ObservedOutputSchemas {
249
+ entries = new Map();
250
+ cacheKey(connectorId, definition) {
251
+ return JSON.stringify([connectorId, definition.name]);
252
+ }
253
+ set(key, entry) {
254
+ this.entries.delete(key);
255
+ this.entries.set(key, entry);
256
+ while (this.entries.size > MAX_CACHED_SHAPES) {
257
+ const oldest = this.entries.keys().next().value;
258
+ if (oldest === undefined)
259
+ break;
260
+ this.entries.delete(oldest);
261
+ }
262
+ }
263
+ get(connectorId, definition) {
264
+ try {
265
+ const key = this.cacheKey(connectorId, definition);
266
+ if (definition.outputSchema) {
267
+ this.entries.delete(key);
268
+ return undefined;
269
+ }
270
+ if (!isExplicitlyReadOnly(definition)) {
271
+ this.entries.delete(key);
272
+ return undefined;
273
+ }
274
+ const identity = definitionIdentity(definition);
275
+ if (!identity) {
276
+ this.entries.delete(key);
277
+ return undefined;
278
+ }
279
+ const entry = this.entries.get(key);
280
+ if (!entry)
281
+ return undefined;
282
+ if (entry.definition !== identity || entry.expiresAt <= Date.now()) {
283
+ this.entries.delete(key);
284
+ return undefined;
285
+ }
286
+ this.entries.delete(key);
287
+ this.entries.set(key, entry);
288
+ return cloneSchema(entry.schema);
289
+ }
290
+ catch {
291
+ return undefined;
292
+ }
293
+ }
294
+ observe(connectorId, definition, value) {
295
+ try {
296
+ const key = this.cacheKey(connectorId, definition);
297
+ if (definition.outputSchema) {
298
+ this.entries.delete(key);
299
+ return;
300
+ }
301
+ if (!isExplicitlyReadOnly(definition)) {
302
+ this.entries.delete(key);
303
+ return;
304
+ }
305
+ const identity = definitionIdentity(definition);
306
+ const inferred = observedSchema(value);
307
+ if (!identity) {
308
+ this.entries.delete(key);
309
+ return;
310
+ }
311
+ if (!inferred)
312
+ return;
313
+ const current = this.entries.get(key);
314
+ const merged = boundSchema(current &&
315
+ current.definition === identity &&
316
+ current.expiresAt > Date.now()
317
+ ? mergeSchemas(current.schema, inferred)
318
+ : inferred, { nodes: 0 });
319
+ if (!merged || !serializedSchema(merged))
320
+ return;
321
+ this.set(key, {
322
+ definition: identity,
323
+ expiresAt: Date.now() + OBSERVATION_TTL_MS,
324
+ schema: merged,
325
+ });
326
+ }
327
+ catch {
328
+ // Observation is an optimization. A provider success stays successful.
329
+ }
330
+ }
331
+ }
package/dist/skills.js CHANGED
@@ -23,13 +23,13 @@ The minimum guest API is:
23
23
 
24
24
  Search inside the run and finish the task there. A discovery-only program wastes a round trip. Use 2–4 distinctive action/object terms, not the full request. Use separate short searches for distinct operations.
25
25
 
26
- For top-level \`search_tools\`, omit \`limit\` initially (the default is 10), then page with a limit up to 50 if needed. Empty or whitespace-only queries browse all tools. A non-empty query with no ASCII terms returns no matches; mixed input searches with its ASCII terms. \`includeSchemas: "compact"\` adds bounded input and declared output shapes. Plain objects expose \`inputKeys\`, \`requiredInputKeys\`, and \`outputKeys\`; truncation flags mark incomplete shapes; matches also carry declared annotations.
26
+ For top-level \`search_tools\`, omit \`limit\` initially (the default is 10), then page with a limit up to 50 if needed. Empty or whitespace-only queries browse all tools. A non-empty query with no ASCII terms returns no matches; mixed input searches with its ASCII terms. \`includeSchemas: "compact"\` adds bounded input and available output shapes. An observed shape carries \`outputSchemaSource: "observed"\`; treat it as routing evidence rather than a provider contract. Plain objects expose \`inputKeys\`, \`requiredInputKeys\`, and \`outputKeys\`; truncation flags mark incomplete shapes; matches also carry declared annotations.
27
27
 
28
28
  - \`connecta.search({})\` loads all catalogs. Pass \`connector: "<id>"\` when the integration is obvious. Use \`safety: "readOnly"\` for program calls. These inputs filter discovery; they grant no authority.
29
- - Request \`includeSchemas: "compact"\`. Check address, purpose, annotations, required inputs, truncation, safety, and declared outputs. Never select only because a result ranks first or has fewer required inputs.
29
+ - Request \`includeSchemas: "compact"\`. Check address, purpose, annotations, required inputs, truncation, safety, and available outputs. Never select only because a result ranks first or has fewer required inputs.
30
30
  - Supply every \`requiredInputKey\` from the task or a prior result. For dependencies, match the earlier \`outputKey\` to the later required key. An empty required-key list does not permit invented arguments. Missing \`outputKeys\` means inspect \`outputSchema\`.
31
31
  - Use \`connecta.describe({ address })\` or \`{ addresses }\` when a compact schema is truncated or insufficient. Use \`format: "json"\` only for exact constraints. Write the property names the schema displays; never guess positions or aliases.
32
- - Reduce through declared output keys. Do not guess collection roots such as \`items\` or \`results\`. If a match or result key is missing, inspect, re-search, or describe inside the same run instead of returning discovery for another call.
32
+ - Reduce through available output keys. Treat an observed key as a hint, since later results may omit it or add others. Do not guess collection roots such as \`items\` or \`results\`. If a match or result key is missing, inspect, re-search, or describe inside the same run instead of returning discovery for another call.
33
33
 
34
34
  Only tools explicitly annotated \`readOnlyHint: true\` are reachable. The catalog, credential, admission, and read-only gates run below the sandbox; code cannot widen its authority.
35
35
 
package/dist/version.d.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export declare const CONNECTA_VERSION = "0.18.2";
7
+ export declare const CONNECTA_VERSION = "0.18.3";
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.18.2";
7
+ export const CONNECTA_VERSION = "0.18.3";
@@ -14,8 +14,9 @@ stating before anything else.
14
14
 
15
15
  **Per isolate, built once.** `createConnecta(config)` returns
16
16
  `{ fetch, registry, close }`. The `Registry` owns the connector set, address
17
- resolution, catalog caches, connector health, and the per-connector call
18
- limiters. It is constructed once and lives as long as the isolate or process —
17
+ resolution, catalog caches, observed output schemas, connector health, and the
18
+ per-connector call limiters. It is constructed once and lives as long as the
19
+ isolate or process —
19
20
  on Workers that means a lazy module-scope singleton, which is why both
20
21
  deployment shapes build it outside the request handler.
21
22
 
@@ -94,6 +95,7 @@ owns or hands out, and a change usually belongs in exactly one of them:
94
95
  | `src/catalog-service.ts` | Request-local tool listing, search, and describe. It coalesces reads inside one request and opts agent reads into the runtime's deferred catalog channel when one exists. |
95
96
  | `src/invocation.ts` | One tool call: argument validation, call admission, per-attempt timeout, retry with the connector's own `Retry-After` honoured exactly or declined, result unwrapping, size capping, and the activity record. |
96
97
  | `src/catalog.ts` | Ranking, description summarizing, and the compact schema renderer discovery shows. |
98
+ | `src/result-shapes.ts` | Bounded runtime-only inference and merging for output shapes learned from successful read-only calls whose providers declared none. |
97
99
 
98
100
  `src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same
99
101
  three services. That is the point: a program's `connecta.call` and a top-level
@@ -142,6 +144,7 @@ src/
142
144
  registry.ts connector set, addresses, health, call limiters
143
145
  catalog-service.ts request-local catalog access, search, and describe
144
146
  catalog.ts ranking, summaries, compact schema rendering
147
+ result-shapes.ts passive runtime-only observed output schemas
145
148
  invocation.ts one tool call, end to end
146
149
  catalog-drift.ts vetted manifests and the counts a refresh produces
147
150
  credentials.ts the AES-GCM connector vault over KVStorage
@@ -149,10 +149,7 @@ a cycle, a `BigInt`, a function, a class instance — never round-trips: it eith
149
149
  ends the run with an error or is converted lossily, executor's choice (`X9`).
150
150
  Return JSON-shaped data and the question does not arise.
151
151
 
152
- **P4.** Nothing survives an execution. There is no module scope, cache, or
153
- scratch storage carried to the next program, and no request-bound object outlives
154
- the request that created it. Within one execution, host calls share one
155
- downstream request scope.
152
+ **P4.** Nothing survives an execution. There is no module scope, cache, or scratch storage carried to the next program, and no request-bound object outlives the request that created it. Within one execution, host calls share one downstream request scope. `S9`'s host-owned output observation is catalog metadata, not guest memory: a later program receives no prior value or object, only a labeled field/type schema through discovery.
156
153
 
157
154
  **P5.** Plain JavaScript only. TypeScript syntax is a syntax error. Portable code
158
155
  does not import: QuickJS blocks imports, while Dynamic Workers expose the `X5`
@@ -223,7 +220,7 @@ const page = await connecta.search({
223
220
  });
224
221
  ```
225
222
 
226
- **S1.** Returns one flat page: `{ tools, total, offset, limit, hasMore }`, plus `nextOffset` when more remains and `matchMode: "partial"` when no tool matched every term. Top-level `search_tools` is different: it returns `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`. Complete matches normally precede partial matches, but a partial candidate whose complete normalized tool name occurs in the normalized raw query competes by score; conversational cleanup applies only to scoring terms. Other candidates covering at least two terms fill the page after every complete match; when no complete match exists, the existing any-term fallback remains. Each entry in `tools` carries `address`, `name`, and — when requested — `description`, `inputSchema`, `outputSchema`, `annotations`, and the connector's `guide`. Tool rows expose neither lexical scores nor per-result coverage. An empty or whitespace-only query browses. Non-empty input with no ASCII lexical terms returns no tools and bounded no-match analysis; mixed input searches with its ASCII terms. Compact shapes omit property prose, put required fields first, and cap each shape at 1,024 UTF-8 bytes. Each enum node gets 256 of those bytes. About three near-cap enum nodes can therefore coexist while leaving the final quarter for surrounding syntax; the unchanged global fallback still applies above 1,024 bytes. A capped enum preserves whole values before `unknown` and an exact omitted-value count, while an empty enum renders as `never`. Either cap carries `inputSchemaTruncated` or `outputSchemaTruncated`; a shape-wide cap remains structurally valid with `unknown` types plus `/* truncated */`. Small enums remain complete. Use `connecta.describe` (or JSON search) for omitted exact constraints.
223
+ **S1.** Returns one flat page: `{ tools, total, offset, limit, hasMore }`, plus `nextOffset` when more remains and `matchMode: "partial"` when no tool matched every term. Top-level `search_tools` is different: it returns `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`. Complete matches normally precede partial matches, but a partial candidate whose complete normalized tool name occurs in the normalized raw query competes by score; conversational cleanup applies only to scoring terms. Other candidates covering at least two terms fill the page after every complete match; when no complete match exists, the existing any-term fallback remains. Each entry in `tools` carries `address`, `name`, and — when requested — `description`, `inputSchema`, `outputSchema`, `annotations`, and the connector's `guide`. An output shape learned under `S9` also carries `outputSchemaSource: "observed"`; provider declarations carry no source marker. Tool rows expose neither lexical scores nor per-result coverage. An empty or whitespace-only query browses. Non-empty input with no ASCII lexical terms returns no tools and bounded no-match analysis; mixed input searches with its ASCII terms. Compact shapes omit property prose, put required fields first, and cap each shape at 1,024 UTF-8 bytes. Each enum node gets 256 of those bytes. About three near-cap enum nodes can therefore coexist while leaving the final quarter for surrounding syntax; the unchanged global fallback still applies above 1,024 bytes. A capped enum preserves whole values before `unknown` and an exact omitted-value count, while an empty enum renders as `never`. Either cap carries `inputSchemaTruncated` or `outputSchemaTruncated`; a shape-wide cap remains structurally valid with `unknown` types plus `/* truncated */`. Small enums remain complete. Use `connecta.describe` (or JSON search) for omitted exact constraints.
227
224
 
228
225
  **S1a.** `connector` loads only the named catalog; omit it only when the integration is ambiguous, because an unscoped search fans out across every configured connector. `safety: "readOnly"` returns exactly the tools available through `connecta.call`, connector shortcuts, and `connecta.batch`; `"approvalRequired"` returns the complementary fail-closed class, including false, missing, and contradictory annotations. Omitted or `"all"` preserves the complete catalog. These filters grant no authority and change no admission decision.
229
226
 
@@ -265,7 +262,7 @@ carry a route-aware `nextAction`; a close miss may add three canonical `suggesti
265
262
  Catalog failures add only `retryAfterMs` when known. One bad address never fails the whole call. Each failed entry clamps its
266
263
  caller-authored `address` to 512 UTF-8 bytes with an `…` marker. Entry order
267
264
  correlates a clipped address with its request; successes keep canonical addresses. More than 100
268
- addresses is `invalid_args`; the same 256,000-byte ceiling applies.
265
+ addresses is `invalid_args`; the same 256,000-byte ceiling applies. A success whose output shape came from `S9` carries `outputSchemaSource: "observed"` beside the rendered schema.
269
266
 
270
267
  ### connecta.call
271
268
 
@@ -304,6 +301,8 @@ the batch, and more than ten calls throws.
304
301
 
305
302
  **S8.** Batch and thrown failures share one vocabulary (`E1`): an entry's `errorDetails.code` and `retryable` equal the fields on the error the same call would throw. Use batch for independent concurrency, not to recover lost type.
306
303
 
304
+ **S9.** A successful explicitly read-only call whose provider declared no `outputSchema` passively learns one from the unwrapped result. The observation retains field names and broad JSON types only: no arguments, scalar values, raw results, code, credentials, or errors. Property names may be user-authored. Objects stay open, every field stays optional, and search or describe labels the shape `outputSchemaSource: "observed"` so a model cannot mistake runtime evidence for a provider contract. Later observations merge fields and types in a process-local 256-entry LRU; a provider declaration always wins. Inference stops at depth 6, 128 schema nodes, 48 properties per object, 32 inspected array items, and 128 UTF-8 bytes per property name; `__proto__`, `constructor`, and `prototype` names are discarded. A tool definition over 64 KiB or an observed schema over 16 KiB is ignored. An entry expires after 24 hours and carries the exact serialized tool definition, so a changed catalog entry, process restart, or Worker isolate eviction starts cold. A failed call or failed result-processing step learns nothing, and any observation failure is discarded without changing a successful call. No discovery read, timer, refresh, background job, or storage adapter executes or persists work for this cache: the result-sampling refusal in [#282](https://github.com/zackbart/connecta/issues/282) stands.
305
+
307
306
  ### connecta.emit
308
307
 
309
308
  ```js
@@ -847,6 +846,7 @@ the upstream `Executor` shape assignable.
847
846
  | `S6` | `test/execute.test.ts` (fail-closed annotations, activity parity) |
848
847
  | `S7` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (batch cap) |
849
848
  | `S8`, `E1`, `X11` | both guest-contract executors (caught call, namespace, discovery, utility, batch-validation, budget, and forgery cases; typed batch equivalence) |
849
+ | `S9` | `test/result-shapes.test.ts` (value exclusion, bounds, merging, LRU and time expiry, runtime isolation, read-only admission, declared precedence, definition invalidation, unwrapped MCP results, discovery provenance, copy isolation, and failure isolation) |
850
850
  | `E2`, `E8` | `test/guest-api-contract.test.ts` (code → `retryable`, caught, batch, and uncaught validation recovery), `test/meta-tools.test.ts` (direct, destructive, batch, provider fallback), `test/validate.test.ts` (bounded payload-free findings), `test/errors.test.ts` |
851
851
  | `E3` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`auth_required`) |
852
852
  | `E4` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (destructive) |
@@ -268,6 +268,16 @@ Tool calls must use the shared invocation path. That keeps direct calls, batch
268
268
  children, and code-mode host calls aligned on safety, retries, admission,
269
269
  timeouts, validation, result guards, and typed failures.
270
270
 
271
+ That path also learns an observed output schema after a successful explicitly
272
+ read-only call when the catalog declared none. The observation retains field
273
+ names and broad JSON types rather than a result. Object fields stay optional,
274
+ additional fields stay allowed, discovery labels the source `observed`, and any
275
+ later provider declaration wins. Property names can be user-authored data, so
276
+ the bounded cache stays in this process or Worker isolate and expires entries
277
+ after 24 hours. An exact tool-definition comparison rejects stale shapes. This
278
+ does not weaken the catalog completeness rule or the refusal of result sampling:
279
+ no catalog read executes a tool.
280
+
271
281
  Connector usage guides are configuration too. `usageGuide` accepts the
272
282
  historical markdown string or `{ content, summary?, required? }`; the latter
273
283
  lets discovery explain what the guide covers without loading it. The summary
@@ -78,21 +78,35 @@ headed to `call_tool` or generated code; `safety: "approvalRequired"` finds the
78
78
  complementary set that must cross `call_destructive_tool`. Omitting `safety`,
79
79
  or setting it to `"all"`, preserves the complete configured catalog. This is
80
80
  only a discovery filter: it neither grants authority nor changes invocation admission.
81
- `includeSchemas: "compact"` adds each match's input and any declared output
82
- shape. Bounded plain-object schemas also expose `inputKeys`,
81
+ `includeSchemas: "compact"` adds each match's input and any provider-declared
82
+ output shape. When the provider declared none but an earlier successful call
83
+ learned one, the same field carries the open observed schema beside
84
+ `outputSchemaSource: "observed"`. That marker matters: observed fields and broad
85
+ JSON types are routing evidence, not a provider contract, and every object field
86
+ remains optional and open to unseen names. A provider declaration always wins.
87
+ Bounded plain-object schemas also expose `inputKeys`,
83
88
  `requiredInputKeys`, and `outputKeys`; a zero-input object keeps
84
89
  `requiredInputKeys: []`, while an output object with no declared properties
85
90
  omits `outputKeys`. A truncated shape omits its corresponding list rather than
86
91
  repeating a large partial inventory. Matches carry declared
87
92
  behavior annotations. Lexical rank is only one signal: select a candidate whose
88
93
  required inputs are available, whose schema is complete enough for the call,
89
- and whose safety and declared outputs fit the work. A reducer uses `outputKeys`
94
+ and whose safety and available outputs fit the work. A reducer uses `outputKeys`
90
95
  before inspecting the value; it does not assume a collection is named `items`
91
96
  or `results`. When that shape is sufficient, call the returned address directly. Reserve schema
92
97
  expansion through `connecta.describe` for a search without schemas, an
93
98
  ambiguous compact shape, or exact
94
99
  constraints that require `format: "json"`.
95
100
 
101
+ Observed schemas originate no provider traffic. A successful explicitly
102
+ read-only call the user already made contributes names and broad types after
103
+ Connecta unwraps the result. Arguments, scalar values, raw results, code,
104
+ credentials, and errors are not retained, though property names may themselves
105
+ be user-authored. Shapes merge in a 256-entry runtime cache for 24 hours under
106
+ the exact tool definition that produced them. A changed definition, process
107
+ restart, or Worker isolate eviction starts cold. Observation cannot fail the
108
+ call, and the declared catalog remains the fallback.
109
+
96
110
  Compact search is deliberately a routing view, not a second copy of connector
97
111
  documentation. Tool purposes are capped at 160 characters, connector
98
112
  descriptions and property prose are omitted, required input fields render
@@ -75,7 +75,7 @@ optional.
75
75
  | `connectors` | — (required) | the connector set ([connectors](./connectors.md)) |
76
76
  | `executor` | — (required) | the sandbox `execute_code` runs in ([code mode](./code-mode.md#what-an-executor-must-implement)) |
77
77
  | `auth?` | none ⇒ open (dev only) | one `InboundAuth` or an array; bearer providers are checked before Clerk ([inbound auth](./auth.md)) |
78
- | `storage?` | `memoryStorage()` | the one state seam ([storage](./storage-and-credentials.md)) |
78
+ | `storage?` | `memoryStorage()` | the one state seam for catalogs, result paging, credentials, and access tokens ([storage](./storage-and-credentials.md)) |
79
79
  | `publicUrl?` | per-request origin | public base URL; an HTTPS value also redirects inbound HTTP |
80
80
  | `logger?` | `console`, prefixed `[connecta]` | `{ debug, info, warn, error }` |
81
81
  | `branding?` | neutral Connecta defaults | operator-page and OAuth result-page labels and marks |
@@ -250,6 +250,7 @@ in.
250
250
  | `remote-mcp-credential.test.ts` | `remoteMcp()` drawing a static key from `/credentials`: the declared slot and its refusal of named fields and bad header names, header framing (bearer, bare, and the two `Basic` forms) observed on the wire, an empty slot failing as `auth_required` rather than reaching the downstream, a value carrying a control character refused before framing and absent from every surface — `call_tool`, `status`, the Test result, `lastError`, and the thrown error — rotation replacing the cached client and a connect already in flight while a wiped value fails the next call, the Test action's catalog probe and scope close, the cleartext-destination warning, and the vault and `authorize_connector` handoff end to end |
251
251
  | `remote-mcp-pagination.test.ts` | the `tools/list` cursor chain in both directions — exact cursor handoff, first-wins dedup, a failed later page rejecting rather than returning its prefix, the runaway backstops, the tool-metadata re-prime across pages, and paginated catalogs reaching the discovery path |
252
252
  | `request-admission.test.ts` | `/mcp` bounded before auth, the stable 503 and `Retry-After`, health and operator responsiveness under saturation, payload-free counters, queued cancellation, shutdown rejection while active work drains, and the separate fallback code pool |
253
+ | `result-shapes.test.ts` | passive output-shape learning: value-free bounded inference, merging, 256-entry LRU eviction, 24-hour expiry, runtime isolation, read-only admission, declared-schema precedence, definition-change invalidation, discovery provenance, and failure isolation |
253
254
  | `revenuecat-provider.test.ts` / `revenuecat-registry.test.ts` | the RevenueCat proxy's per-project key scoping and account-wide OAuth guides, its purpose-bearing summary, the argued borderline verdicts in its digest-free manifest, and the deliberately unclassified `render-paywall-screenshot`; then two project-scoped keys as two connectors in a real deployment |
254
255
  | `server.test.ts` | end-to-end `/mcp` (401 → compact initialize instructions → seven compact definitions with bounded connector inventory → complete usage skill → `call_tool`), conditional guide pointers, open routes, Clerk `.well-known` metadata without network, code mode, and deferred catalog reads through both discovery surfaces |
255
256
  | `server-route-contracts.test.ts` | the route contracts `server.ts` must keep byte-identical: every built-in answered ahead of connector routes inside the security wrapper, open data-free shells with framing denied, per-route auth and same-origin requirements with exact 401/403/405 bodies, and OAuth `verifyState`-before-`finishAuth` ordering |
@@ -57,7 +57,7 @@ exist so far:
57
57
  | --- | --- | --- |
58
58
  | **pre-template** | before 0.10.2 | no `connecta init` existed; hand-written, or copied from the retired `examples/node` |
59
59
  | **A** | 0.10.2 – 0.15.1 | `.env.example`, `.gitignore`, `AGENTS.md`, `CLAUDE.md`, `README.md`, `package.json`, `src/index.ts`, `tsconfig.json` |
60
- | **B** | 0.16.0 – 0.18.2 | adds `.dockerignore`, `Dockerfile`, `docker-compose.yml`, and `src/file-activity.ts`; `src/index.ts` grows the four commented operator blocks; `.env.example` ships `CONNECTA_TOKEN=` empty |
60
+ | **B** | 0.16.0 – 0.18.3 | adds `.dockerignore`, `Dockerfile`, `docker-compose.yml`, and `src/file-activity.ts`; `src/index.ts` grows the four commented operator blocks; `.env.example` ships `CONNECTA_TOKEN=` empty |
61
61
 
62
62
  Generation A is a decade in template years and identifying it precisely does
63
63
  not matter, because you are about to reconstruct it exactly rather than guess
@@ -106,7 +106,7 @@ know what to preserve, once to know what to re-verify at the end.
106
106
  ### Bump the pin and install
107
107
 
108
108
  ```sh
109
- npm pkg set dependencies.@zackbart/connecta=0.18.2
109
+ npm pkg set dependencies.@zackbart/connecta=0.18.3
110
110
  npm install
111
111
  ```
112
112
 
@@ -130,7 +130,7 @@ Generate the *current* template beside the base you already made, into the same
130
130
  `$SCRATCH`:
131
131
 
132
132
  ```sh
133
- (cd "$SCRATCH" && npx @zackbart/connecta@0.18.2 init current)
133
+ (cd "$SCRATCH" && npx @zackbart/connecta@0.18.3 init current)
134
134
  ```
135
135
 
136
136
  You now have a three-way merge with a real base: `$SCRATCH/base` is what this
@@ -186,7 +186,7 @@ A deployment older than 0.10.2 has no base to diff against. Do not try to
186
186
  manufacture one. Instead:
187
187
 
188
188
  1. `SCRATCH=$(mktemp -d)`, then
189
- `(cd "$SCRATCH" && npx @zackbart/connecta@0.18.2 init current)` — there is no
189
+ `(cd "$SCRATCH" && npx @zackbart/connecta@0.18.3 init current)` — there is no
190
190
  `base` leg here, only the current template to read from.
191
191
  2. Copy `$SCRATCH/current` into the deployment file by file, **skipping
192
192
  `src/index.ts`**.
@@ -207,6 +207,17 @@ first, so cross them bottom-up: start at the oldest one still above this
207
207
  deployment's pin and work back up the page, because each boundary assumes the
208
208
  older ones are already done.
209
209
 
210
+ ### 0.18.2 → 0.18.3
211
+
212
+ Nothing throws for an existing deployment, and the version bump alone crosses
213
+ this boundary. Successful explicitly read-only calls whose provider declares
214
+ no output schema now teach later discovery the result's field names and broad
215
+ JSON types. The open optional-field shape is labeled
216
+ `outputSchemaSource: "observed"`, lives only in a bounded runtime cache, and
217
+ starts cold after 24 hours, a process restart, or Worker isolate eviction. No
218
+ configuration or storage migration is involved, and discovery still never
219
+ executes a tool.
220
+
210
221
  ### 0.18.1 → 0.18.2
211
222
 
212
223
  Nothing throws for an existing deployment, and the version bump alone crosses
package/ethos.md CHANGED
@@ -111,11 +111,11 @@ proposing one without a new argument is not.
111
111
  | View-initiated read calls from program UI | accepted | named bindings materially improve refresh, cursor pagination, and drill-down without persistence or a new tool; the trusted shell delegates only to the existing fail-closed `call_tool`, and the one-string UI remains display-only ([evidence](./documentation/program-ui-read-calls.md), [#287](https://github.com/zackbart/connecta/issues/287), [#289](https://github.com/zackbart/connecta/issues/289)) |
112
112
  | View-initiated mutation calls from program UI | gated | live-read utility says nothing about write consent: a click is not approval, stale/replayed effects need a host-tested story, and the ordinary destructive path keeps the action in the transcript ([#287](https://github.com/zackbart/connecta/issues/287)) |
113
113
  | Result sampling on the catalog surface (`sample` / `dryRun`) | refused | sampling is execution and cannot ride a catalog read; most tools carry required arguments no sampler can invent, and undeclared `outputSchema` (measured 0/30 and 3/30 on real deployments) is a real gap that is not a sampleable one — a program that checks the shape before rendering already hands back the first record inside the run it was going to make anyway, at zero new surface ([#282](https://github.com/zackbart/connecta/issues/282)) |
114
+ | Passive observed output schemas | accepted | the sampling refusal stands: discovery originates no call and invents no arguments; instead, a successful explicitly read-only call whose provider declared no `outputSchema` records field names and broad JSON types only, under strict depth, breadth, property-name, node, and byte bounds, then merges that open optional-field shape in a 256-entry process-local cache for 24 hours; property names may be user-authored, search and describe label the shape `outputSchemaSource: "observed"`, a materially changed tool definition cannot inherit it, a provider declaration always wins, and no argument, scalar value, raw result, code, credential, or error is retained; this is new evidence rather than a rewrite of #282's facts: Blacksmith measured a value-free warm shape cache cutting one Linear code-mode task from 116.6 s / $1.91 to 56.6 s / $1.06, while live Connecta catalogs measured 246/378 missing on BePresent and 0/90 missing on OneMany ([study](https://www.blacksmith.sh/blog/code-smith-code-mode), [#442](https://github.com/zackbart/connecta/issues/442)) |
114
115
  | Legacy embedded `UIResource` delivery | refused | superseded upstream and rendered by none of the clients connecta faces; per-request minted URIs also fight the caching the Apps spec assumes ([#266](https://github.com/zackbart/connecta/issues/266)) |
115
116
  ## Invariants
116
117
 
117
- One line each; the enforcing tests live beside the subsystem documentation.
118
- Breaking one is not a bug fix — it is a design change wearing a disguise.
118
+ One line each; the enforcing tests live beside the subsystem documentation. Breaking one is not a bug fix — it is a design change wearing a disguise.
119
119
 
120
120
  - **Fail-closed read-only.** A missing, false, or contradictory annotation
121
121
  never gets the benefit of the doubt.
@@ -131,6 +131,7 @@ Breaking one is not a bug fix — it is a design change wearing a disguise.
131
131
  is never cached, persisted, or served as if it were small.
132
132
  - **Activity is payload-free by construction.** The event type has nowhere to
133
133
  put arguments, results, code, or raw error text.
134
+ - **An observed shape is never a declaration.** It contains field names and broad JSON types only, remains open and optional, is labeled on discovery, and disappears behind any provider-declared output schema.
134
135
  - **Credentials never leave the host.** Encrypted at rest, readable only by
135
136
  the owning connector, never rendered by any surface.
136
137
  - **Import-graph purity.** Nothing reachable from the root entry imports a
@@ -145,5 +146,4 @@ Breaking one is not a bug fix — it is a design change wearing a disguise.
145
146
  - **Structural mistakes throw at construction.** A deployment that boots into
146
147
  the wrong shape is worse than one that refuses to boot.
147
148
 
148
- ---
149
149
  Connecta began as a radical simplification of [executor](https://github.com/UsefulSoftwareCo/executor); the table above is the record of that simplification holding.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zackbart/connecta",
3
- "version": "0.18.2",
3
+ "version": "0.18.3",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of seven meta-tools.",
@@ -15,7 +15,7 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@zackbart/connecta": "0.18.2",
18
+ "@zackbart/connecta": "0.18.3",
19
19
  "quickjs-emscripten": "0.32.0"
20
20
  },
21
21
  "devDependencies": {