@zackbart/connecta 0.39.0 → 0.41.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 (120) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/bin/connecta.mjs +4 -8
  3. package/bin/version.mjs +1 -1
  4. package/dist/access-tokens.js +6 -4
  5. package/dist/activity.d.ts +30 -4
  6. package/dist/activity.js +28 -2
  7. package/dist/agent-observation.js +24 -2
  8. package/dist/agent-routes.js +4 -41
  9. package/dist/auth/clerk.d.ts +6 -1
  10. package/dist/auth/clerk.js +16 -0
  11. package/dist/auth/cloudflare-access.js +49 -7
  12. package/dist/auth/downstream-oauth/consent.js +79 -29
  13. package/dist/auth/downstream-oauth/grant-store.js +58 -7
  14. package/dist/auth/downstream-oauth/refresh-records.js +6 -5
  15. package/dist/auth/downstream-oauth.js +12 -4
  16. package/dist/auth/native.js +10 -0
  17. package/dist/auth/oauth-request-not-sent.js +4 -0
  18. package/dist/auth/static-oauth.js +51 -21
  19. package/dist/catalog-cache.js +151 -37
  20. package/dist/catalog-freshness.js +13 -0
  21. package/dist/catalog-service.js +39 -12
  22. package/dist/catalog.js +103 -38
  23. package/dist/config.d.ts +10 -4
  24. package/dist/config.js +30 -7
  25. package/dist/connectors/remote-mcp/call.js +1 -1
  26. package/dist/connectors/remote-mcp/connection.js +52 -7
  27. package/dist/connectors/remote-mcp/oauth.js +6 -2
  28. package/dist/connectors/remote-mcp/resources.js +5 -2
  29. package/dist/connectors/remote-mcp/transport.js +37 -8
  30. package/dist/connectors/remote-mcp.js +8 -5
  31. package/dist/credential-contract.d.ts +3 -1
  32. package/dist/describe-config.d.ts +1 -0
  33. package/dist/describe-config.js +1 -0
  34. package/dist/downstream-input.js +11 -2
  35. package/dist/errors.d.ts +8 -1
  36. package/dist/errors.js +2 -0
  37. package/dist/execute.js +20 -7
  38. package/dist/executor-result.js +1 -1
  39. package/dist/executors/quickjs-child.js +1 -1
  40. package/dist/executors/quickjs-runtime.js +1 -1
  41. package/dist/executors/quickjs.js +1 -1
  42. package/dist/guarded-transport.js +2 -0
  43. package/dist/http/client.js +7 -0
  44. package/dist/http/hatch.js +2 -0
  45. package/dist/http/node-transport.js +110 -4
  46. package/dist/http/oauth.js +17 -6
  47. package/dist/index.js +10 -21
  48. package/dist/invocation-failure.js +28 -0
  49. package/dist/invocation.js +77 -35
  50. package/dist/membership.d.ts +1 -1
  51. package/dist/meta-tools.js +58 -36
  52. package/dist/module-contracts.d.ts +3 -1
  53. package/dist/native-tools.js +12 -5
  54. package/dist/node.d.ts +7 -0
  55. package/dist/node.js +82 -3
  56. package/dist/operation-activity.js +103 -0
  57. package/dist/operator-record.d.ts +8 -0
  58. package/dist/operator-record.js +15 -0
  59. package/dist/operator-ui/generated.js +2 -2
  60. package/dist/provider-call-facts.js +6 -2
  61. package/dist/provider.d.ts +1 -1
  62. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  63. package/dist/providers/breeze/index.js +1 -1
  64. package/dist/providers/ccb/index.js +1 -1
  65. package/dist/providers/cloudflare/index.js +1 -1
  66. package/dist/providers/docs/index.js +1 -1
  67. package/dist/providers/drive/index.js +1 -1
  68. package/dist/providers/forms/index.js +1 -1
  69. package/dist/providers/github/index.js +1 -1
  70. package/dist/providers/gmail/index.js +1 -1
  71. package/dist/providers/granola/index.js +1 -1
  72. package/dist/providers/overflow/index.js +1 -1
  73. package/dist/providers/planning-center/index.js +1 -1
  74. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  75. package/dist/providers/quickbooks/api.js +28 -25
  76. package/dist/providers/quickbooks/index.js +1 -1
  77. package/dist/providers/quickbooks/lib/writes.js +55 -0
  78. package/dist/providers/quickbooks/safety.js +17 -5
  79. package/dist/providers/quickbooks/skill.generated.js +1 -1
  80. package/dist/providers/sheets/index.js +1 -1
  81. package/dist/providers/slides/index.js +1 -1
  82. package/dist/providers/stripe/index.js +2 -2
  83. package/dist/providers/tithely/index.js +1 -1
  84. package/dist/providers/vercel/api.js +1 -0
  85. package/dist/registry.js +16 -4
  86. package/dist/resource-read.js +21 -2
  87. package/dist/route-names.js +44 -0
  88. package/dist/routes/activity-context.js +1 -0
  89. package/dist/routes/activity-disclosure.js +26 -4
  90. package/dist/routes/activity.js +2 -1
  91. package/dist/routes/mcp.js +32 -13
  92. package/dist/routes/ui-config.js +1 -1
  93. package/dist/routes/ui-data.js +3 -2
  94. package/dist/runtime/native-storage.js +15 -0
  95. package/dist/runtime/request-reads.js +87 -10
  96. package/dist/runtime/request-scope.js +2 -1
  97. package/dist/skill-origin.js +16 -0
  98. package/dist/skills.js +132 -43
  99. package/dist/storage/d1-driver.js +33 -5
  100. package/dist/storage/keys.js +7 -1
  101. package/dist/storage/sql-native.js +85 -3
  102. package/dist/storage/sql.js +6 -0
  103. package/dist/timing-trace.js +84 -0
  104. package/dist/types.d.ts +5 -0
  105. package/dist/url-safety.js +38 -20
  106. package/dist/utility-recovery.js +33 -0
  107. package/dist/version.d.ts +1 -1
  108. package/dist/version.js +1 -1
  109. package/dist/worker.js +33 -8
  110. package/documentation/architecture.md +83 -2
  111. package/documentation/auth.md +132 -37
  112. package/documentation/code-mode.md +61 -46
  113. package/documentation/deploying.md +25 -1
  114. package/documentation/integrating.md +2 -2
  115. package/documentation/meta-tools.md +134 -15
  116. package/documentation/operating.md +86 -1
  117. package/documentation/operator-ui.md +51 -2
  118. package/documentation/providers.md +16 -14
  119. package/package.json +10 -8
  120. package/templates/node/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { checkedActivityOperation } from "../operation-activity.js";
1
2
  import { recordedToolName } from "../operator-record.js";
2
3
  /** A recorded tool is shown only when its name passes the record grammar and its endpoint granted it. */
3
4
  function activityToolVisible(access, event) {
@@ -15,6 +16,31 @@ function activityToolVisible(access, event) {
15
16
  * pool that is no longer declared is withheld.
16
17
  */
17
18
  export function activityEventVisible(context, authz, registry, event) {
19
+ const pool = event.pool !== undefined ? context.opts.pools.get(event.pool) : undefined;
20
+ if (event.pool !== undefined && !pool)
21
+ return false;
22
+ const access = pool ? pool.access : { connectorIds: "all" };
23
+ if (event.kind === "operation") {
24
+ const operation = checkedActivityOperation(event.operation);
25
+ if (!operation ||
26
+ event.toolName !== operation.name ||
27
+ event.connectorId !== (operation.connectors.length === 1 ? operation.connectors[0] : "$operation"))
28
+ return false;
29
+ const ownerOnly = operation.personal ||
30
+ event.actorBasis === "principal" ||
31
+ operation.connectors.some((id) => registry.getConnector(id)?.authScope === "personal");
32
+ if (ownerOnly) {
33
+ const owner = authz.identity.principal;
34
+ if (!owner ||
35
+ event.actorBasis !== "principal" ||
36
+ event.actor.id !== owner.id ||
37
+ event.actor.namespace !== owner.namespace)
38
+ return false;
39
+ }
40
+ return operation.connectors.every((id) => registry.canReadConnectorSkills(id) &&
41
+ (access.connectorIds === "all" || access.connectorIds.includes(id)) &&
42
+ !access.toolAccess?.has(id));
43
+ }
18
44
  const connector = registry.getConnector(event.connectorId);
19
45
  if (!connector)
20
46
  return false;
@@ -26,10 +52,6 @@ export function activityEventVisible(context, authz, registry, event) {
26
52
  event.actor.namespace !== owner.namespace)
27
53
  return false;
28
54
  }
29
- const pool = event.pool !== undefined ? context.opts.pools.get(event.pool) : undefined;
30
- if (event.pool !== undefined && !pool)
31
- return false;
32
- const access = pool ? pool.access : { connectorIds: "all" };
33
55
  if (access.connectorIds !== "all" && !access.connectorIds.includes(event.connectorId))
34
56
  return false;
35
57
  // Connector-wide counts reveal tools outside an address-only grant.
@@ -27,12 +27,13 @@ function enrichActivityActorLabels(page, auth) {
27
27
  ...(event.actor.namespace ? { namespace: event.actor.namespace } : {}),
28
28
  };
29
29
  // Re-check stored client facts, including custom readers and old rows.
30
- const { packageVersion: suppliedPackageVersion, clientName: suppliedName, clientVersion: suppliedVersion, classification: _classification, resultBytes: _resultBytes, kind: _kind, drift: _drift, pool: _pool, actorBasis: _actorBasis, ...record } = event;
30
+ const { packageVersion: suppliedPackageVersion, clientName: suppliedName, clientVersion: suppliedVersion, classification: _classification, resultBytes: _resultBytes, kind: _kind, drift: _drift, operation: _operation, friction: suppliedFriction, pool: _pool, actorBasis: _actorBasis, ...record } = event;
31
31
  const packageVersion = activityPackageVersion(suppliedPackageVersion);
32
32
  const clientName = activityClientFact(suppliedName, "name");
33
33
  const clientVersion = activityClientFact(suppliedVersion, "version");
34
34
  return {
35
35
  ...record,
36
+ ...(event.kind !== "operation" && suppliedFriction !== undefined ? { friction: suppliedFriction } : {}),
36
37
  ...activityBehaviorFacts(event),
37
38
  ...(packageVersion !== undefined ? { packageVersion } : {}),
38
39
  ...(clientName !== undefined ? { clientName } : {}),
@@ -1,3 +1,4 @@
1
+ import { startTimingTrace } from "../timing-trace.js";
1
2
  import { routeNames } from "../agent-routes.js";
2
3
  import { upwardStep } from "../upward.js";
3
4
  import { bindDiscoveryEndpoint } from "../discovery-health.js";
@@ -128,7 +129,7 @@ function withMcpCors(response, request, allowedOrigin) {
128
129
  }
129
130
  headers.append("Vary", "Access-Control-Request-Headers");
130
131
  }
131
- headers.set("Access-Control-Expose-Headers", "WWW-Authenticate, Retry-After, mcp-session-id, mcp-protocol-version, Connecta-Error-Code, Connecta-Recovery");
132
+ headers.set("Access-Control-Expose-Headers", "WWW-Authenticate, Retry-After, mcp-session-id, mcp-protocol-version, Connecta-Error-Code, Connecta-Recovery, Connecta-Next-Action");
132
133
  const wrapped = new Response(response.body, {
133
134
  status: response.status,
134
135
  statusText: response.statusText,
@@ -359,10 +360,11 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
359
360
  // A request-local server cannot publish catalog changes. Set this before
360
361
  // tool registration, whose SDK default otherwise advertises listChanged.
361
362
  capabilities: { tools: { listChanged: false }, extensions: { "io.modelcontextprotocol/skills": {} } },
362
- // Instructions differ per endpoint only by the admitted pool's
363
- // description. Every cache hint below is private, so a client keeps
363
+ // Instructions reflect the admitted pool and effective write policy. Every cache hint below is private, so a client keeps
364
364
  // what it learned per endpoint URL and never shares it across them.
365
- instructions: endpointInstructions(pool !== undefined ? opts.pools.get(pool)?.description : undefined, native),
365
+ instructions: endpointInstructions(pool !== undefined ? opts.pools.get(pool)?.description : undefined, native, readOnly ||
366
+ (registry.listConnectors().length > 0 &&
367
+ registry.listConnectors().every((connector) => connector.readOnly === true))),
366
368
  inputRequired: { legacyShim: false },
367
369
  requestState: { verify: (state, context) => authElicitation.verify(state, context) },
368
370
  cacheHints: {
@@ -378,6 +380,7 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
378
380
  ? {
379
381
  sink: opts.config.activity?.store,
380
382
  recordTool: opts.config.activity?.recordTool,
383
+ recordOperation: opts.config.activity?.recordOperation,
381
384
  actor,
382
385
  ...(principalActor ? { principalActor } : {}),
383
386
  requestId: crypto.randomUUID(),
@@ -635,16 +638,22 @@ export function createMcpRoute(opts) {
635
638
  request.signal.addEventListener("abort", onCallerAbort, { once: true });
636
639
  if (request.signal.aborted)
637
640
  onCallerAbort();
641
+ const finishTiming = startTimingTrace(requestScope, opts.config.timingTraceSampleRate, opts.config.logger);
638
642
  let deadlineTimer;
639
643
  return scopedToBody(Effect.gen(function* () {
640
644
  yield* Effect.addFinalizer(() => Effect.sync(() => {
641
- requestOwner(requestScope).close();
642
- if (!localAbort.signal.aborted) {
643
- localAbort.abort(request.signal.reason ?? new Error("MCP request ended."));
645
+ try {
646
+ requestOwner(requestScope).close();
647
+ }
648
+ finally {
649
+ finishTiming?.();
650
+ if (!localAbort.signal.aborted) {
651
+ localAbort.abort(request.signal.reason ?? new Error("MCP request ended."));
652
+ }
653
+ request.signal.removeEventListener("abort", onCallerAbort);
654
+ if (deadlineTimer !== undefined)
655
+ clearTimeout(deadlineTimer);
644
656
  }
645
- request.signal.removeEventListener("abort", onCallerAbort);
646
- if (deadlineTimer !== undefined)
647
- clearTimeout(deadlineTimer);
648
657
  }));
649
658
  // A request naming `subscriptions/listen` defers its permit: nothing of
650
659
  // its body is read until auth and endpoint admission pass, and then at
@@ -652,7 +661,9 @@ export function createMcpRoute(opts) {
652
661
  // modern listen goes without a permit; trusting the header alone would
653
662
  // let a legacy tools/call bypass the pool, so anything else takes one
654
663
  // after classification. Every other request takes its permit here,
655
- // before auth, and no body is read before auth on any path.
664
+ // before auth, and this route reads no body before auth on any path.
665
+ // The Node adapter has already buffered up to its own pre-auth cap
666
+ // (`listen`'s `maxBodyBytes`) before a request reaches core.
656
667
  const startedAt = Date.now();
657
668
  const maxDurationMs = opts.requestAdmission.maxDurationMs;
658
669
  const listenHeader = request.method === "POST" && request.headers.get("Mcp-Method") === "subscriptions/listen";
@@ -766,8 +777,12 @@ export function createMcpRoute(opts) {
766
777
  const headers = new Headers(response.headers);
767
778
  headers.set("Connecta-Error-Code", "host_auth_required");
768
779
  headers.set("Connecta-Recovery", "host_connection");
769
- // Custom adapters may own a streaming or non-JSON response. Preserve
770
- // that body and its request lifetime, and carry recovery in headers.
780
+ headers.set("Connecta-Next-Action", JSON.stringify({
781
+ action: "reconnect_host",
782
+ purpose: "Reconnect the host with valid inbound credentials and endpoint grants; downstream OAuth cannot repair this connection.",
783
+ }));
784
+ // Custom adapters own their response stream and request lifetime.
785
+ // Their typed recovery is carried in headers instead of replacing it.
771
786
  if (!response.headers.get("Content-Type")?.includes("application/json")) {
772
787
  return cors(new Response(response.body, { status: response.status, headers }));
773
788
  }
@@ -780,6 +795,10 @@ export function createMcpRoute(opts) {
780
795
  message: "The host is not authorized to connect to this endpoint. Sign in or update the host's connecta access token and endpoint grants.",
781
796
  retryable: false,
782
797
  recovery: "host_connection",
798
+ nextAction: {
799
+ action: "reconnect_host",
800
+ purpose: "Reconnect this host to Connecta with valid inbound credentials and endpoint grants. Downstream connector authorization cannot repair the host connection.",
801
+ },
783
802
  },
784
803
  })), { status: response.status, headers }));
785
804
  }
@@ -171,7 +171,7 @@ export function scanOperatorActivity(context, authz, registry, rows, agents) {
171
171
  continue;
172
172
  // A last call is a call; any row recorded for an agent, a catalog
173
173
  // refresh included, is a sign it was seen.
174
- const row = event.kind !== "catalog_drift" && OUTCOMES.has(event.outcome) ? byId.get(event.connectorId) : undefined;
174
+ const row = event.kind === undefined && OUTCOMES.has(event.outcome) ? byId.get(event.connectorId) : undefined;
175
175
  const agent = (event.actor.kind === "access_token" || event.actor.kind === "vercel_oidc") &&
176
176
  event.actor.namespace === AGENT_NAMESPACE &&
177
177
  event.actor.id
@@ -249,6 +249,7 @@ export function uiData(registry, baseUrl, options) {
249
249
  toolCount,
250
250
  observedAt: Date.now(),
251
251
  }));
252
+ const observedAgeMs = observation ? Math.max(0, Date.now() - observation.observedAt) : undefined;
252
253
  return {
253
254
  id: c.id,
254
255
  authScope: c.authScope ?? "shared",
@@ -271,9 +272,9 @@ export function uiData(registry, baseUrl, options) {
271
272
  ...(status.registrationPath ? { auth: { registrationPath: status.registrationPath } } : {}),
272
273
  catalogAgeMs: registry.catalogAgeMs(c.id),
273
274
  lastCall: null,
274
- ...(observation ? { observedAgeMs: Math.max(0, Date.now() - observation.observedAt) } : {}),
275
+ ...(observedAgeMs !== undefined ? { observedAgeMs } : {}),
275
276
  },
276
- ...(observation ? { observedAgeMs: Math.max(0, Date.now() - observation.observedAt) } : {}),
277
+ ...(observedAgeMs !== undefined ? { observedAgeMs } : {}),
277
278
  // Counts only, and only when a refresh in this runtime produced them.
278
279
  // `Registry.withCatalogObservations` rebuilt the report through
279
280
  ...(status.catalogAccess ? { catalogAccess: status.catalogAccess } : {}),
@@ -25,6 +25,14 @@ export function registerNativeStorage(edge, native) {
25
25
  storageMethods.set(edge, own);
26
26
  const getMany = native.getMany;
27
27
  storage.set(edge, {
28
+ catalogSnapshot: native.catalogSnapshot
29
+ ? (keys, generationKey, prefix, suffixes) => typed(() => getNativeStorage(edge)
30
+ ? native.catalogSnapshot(keys, generationKey, prefix, suffixes)
31
+ : Effect.succeed(new Map()))
32
+ : undefined,
33
+ accessToken: native.accessToken
34
+ ? (hash) => typed(() => (getNativeStorage(edge) ? native.accessToken(hash) : Effect.succeed(undefined)))
35
+ : undefined,
28
36
  get: (key) => current(edge, "get", own.get, [key], () => native.get(key)),
29
37
  // A replaced `get` answers each key itself, in order, as before.
30
38
  ...(getMany
@@ -34,6 +42,13 @@ export function registerNativeStorage(edge, native) {
34
42
  : Effect.forEach(keys, (key) => current(edge, "get", own.get, [key], () => native.get(key)))),
35
43
  }
36
44
  : {}),
45
+ ...(native.setMany
46
+ ? {
47
+ setMany: (entries, options) => typed(() => getNativeStorage(edge)
48
+ ? native.setMany(entries, options)
49
+ : Effect.asVoid(Effect.forEach(entries, ([key, value]) => current(edge, "set", own.set, [key, value, options], () => native.set(key, value, options))))),
50
+ }
51
+ : {}),
37
52
  set: (key, value, options) => current(edge, "set", own.set, [key, value, options], () => native.set(key, value, options)),
38
53
  delete: (key) => current(edge, "delete", own.delete, [key], () => native.delete(key)),
39
54
  list: (prefix) => current(edge, "list", own.list, [prefix], () => native.list(prefix)),
@@ -1,3 +1,4 @@
1
+ import { utf8Length } from "../bytes.js";
1
2
  // Request-scoped memo of storage reads (INV-7).
2
3
  //
3
4
  // One request often reads the same key many times: a catalog listing reads its
@@ -36,6 +37,8 @@
36
37
  // `FinalizationRegistry` (workerd before compatibility date 2025-05-05 without
37
38
  // `enable_weak_ref`) memoizes nothing: every read goes to storage.
38
39
  import { Effect } from "effect";
40
+ import { getNativeStorage } from "./native-storage.js";
41
+ const roots = new WeakMap();
39
42
  const observed = new WeakMap();
40
43
  /**
41
44
  * Open memos holding or reading entries of each store. Weak, so a request
@@ -72,6 +75,8 @@ export function observedWrite(store, key, write) {
72
75
  * one is.
73
76
  */
74
77
  export function observeStorageWrites(storage, layer) {
78
+ if ("root" in layer)
79
+ roots.set(layer.root, storage);
75
80
  observed.set(storage, {
76
81
  own: {
77
82
  get: storage.get,
@@ -85,7 +90,7 @@ export function observeStorageWrites(storage, layer) {
85
90
  return storage;
86
91
  }
87
92
  /** The underlying store and key a read of `key` reaches, when every layer is observed. */
88
- function physical(storage, key) {
93
+ export function physicalStorageAddress(storage, key) {
89
94
  const entry = observed.get(storage);
90
95
  if (!entry)
91
96
  return undefined;
@@ -96,10 +101,17 @@ function physical(storage, key) {
96
101
  own.list !== storage.list ||
97
102
  own.compareAndSet !== storage.compareAndSet)
98
103
  return undefined;
99
- return "root" in layer ? { store: layer.root, key } : physical(layer.storage, layer.prefix + key);
104
+ return "root" in layer ? { store: layer.root, key } : physicalStorageAddress(layer.storage, layer.prefix + key);
100
105
  }
101
106
  const weakReferences = () => typeof WeakRef === "function" && typeof FinalizationRegistry === "function";
102
107
  export class RequestReads {
108
+ trace;
109
+ constructor(trace) {
110
+ this.trace = trace;
111
+ }
112
+ note(fresh, hit, value, started) {
113
+ this.trace?.record(fresh ? "memo.fresh" : hit ? "memo.hit" : "memo.miss", performance.now() - started, value === null ? 0 : utf8Length(value), 0, value === null ? 1 : 0);
114
+ }
103
115
  closed = false;
104
116
  stores = new Map();
105
117
  ref = weakReferences() ? new WeakRef(this) : undefined;
@@ -126,7 +138,7 @@ export class RequestReads {
126
138
  }
127
139
  /** A memoized answer, or how to record the value a storage read returns. */
128
140
  lookup(storage, key, fresh) {
129
- const address = this.closed || !this.ref ? undefined : physical(storage, key);
141
+ const address = this.closed || !this.ref ? undefined : physicalStorageAddress(storage, key);
130
142
  if (!address)
131
143
  return undefined;
132
144
  const entries = this.entriesFor(address.store);
@@ -152,22 +164,87 @@ export class RequestReads {
152
164
  read(storage, key, load, fresh = false) {
153
165
  return Effect.suspend(() => {
154
166
  const found = this.lookup(storage, key, fresh);
155
- if (!found)
167
+ if (!found && !this.trace)
156
168
  return load;
157
- if (found.hit)
169
+ const started = this.trace ? performance.now() : 0;
170
+ if (found?.hit) {
171
+ if (this.trace)
172
+ this.note(fresh, true, found.value, started);
158
173
  return Effect.succeed(found.value);
159
- return Effect.tap(load, (value) => Effect.sync(() => found.record(value)));
174
+ }
175
+ return Effect.tap(load, (value) => Effect.sync(() => {
176
+ found?.record(value);
177
+ if (this.trace)
178
+ this.note(fresh, false, value, started);
179
+ }));
180
+ });
181
+ }
182
+ /** Prefetch only keys whose callers already permit request-local staleness. */
183
+ prefetch(requests, load, linked) {
184
+ return Effect.suspend(() => {
185
+ const pending = requests.map(({ storage, key }) => ({
186
+ address: physicalStorageAddress(storage, key),
187
+ found: this.lookup(storage, key, false),
188
+ }));
189
+ if (this.trace)
190
+ for (const { found } of pending)
191
+ if (found?.hit)
192
+ this.note(false, true, found.value, performance.now());
193
+ const started = this.trace ? performance.now() : 0;
194
+ const root = pending[0]?.address?.store;
195
+ const storage = root && roots.get(root);
196
+ if (!storage || pending.some(({ address, found }) => !found || address?.store !== root))
197
+ return Effect.void;
198
+ if (pending.every(({ found }) => found?.hit))
199
+ return Effect.void;
200
+ const generation = linked && physicalStorageAddress(linked.storage, linked.generationKey);
201
+ const prefix = linked && physicalStorageAddress(linked.storage, linked.prefix);
202
+ const snapshot = getNativeStorage(storage)?.catalogSnapshot;
203
+ if (linked && snapshot && generation?.store === root && prefix?.store === root) {
204
+ // Candidates only locate entries. An intervening external write can make
205
+ // these stale, so consumers retain the separate post-chunk primary fence.
206
+ return Effect.map(snapshot(pending.map(({ address }) => address.key), generation.key, prefix.key, linked.suffixes), (values) => {
207
+ pending.forEach(({ address, found }) => {
208
+ if (found && !found.hit)
209
+ found.record(values.get(address.key) ?? null);
210
+ });
211
+ for (const [key, value] of values) {
212
+ if (pending.some(({ address }) => address.key === key))
213
+ continue;
214
+ const found = this.lookup(storage, key, false);
215
+ if (found && !found.hit)
216
+ found.record(value);
217
+ }
218
+ });
219
+ }
220
+ const misses = pending.filter(({ found }) => found && !found.hit);
221
+ if (!misses.length)
222
+ return Effect.void;
223
+ return Effect.map(load(storage, misses.map(({ address }) => address.key)), (values) => {
224
+ misses.forEach(({ found }, i) => {
225
+ if (found && !found.hit) {
226
+ const value = values[i] ?? null;
227
+ found.record(value);
228
+ if (this.trace)
229
+ this.note(false, false, value, started);
230
+ }
231
+ });
232
+ });
160
233
  });
161
234
  }
162
235
  /** `read` for a Promise-shaped caller, without another Effect edge. */
163
236
  async readPromise(storage, key, load, fresh = false) {
164
237
  const found = this.lookup(storage, key, fresh);
165
- if (!found)
166
- return load();
167
- if (found.hit)
238
+ const started = this.trace ? performance.now() : 0;
239
+ if (found?.hit) {
240
+ if (this.trace)
241
+ this.note(fresh, true, found.value, started);
168
242
  return found.value;
243
+ }
169
244
  const value = await load();
170
- found.record(value);
245
+ found?.record(value);
246
+ if (this.trace)
247
+ this.note(fresh, false, value, started);
171
248
  return value;
172
249
  }
173
250
  close() {
@@ -1,3 +1,4 @@
1
+ import { traceFor } from "../timing-trace.js";
1
2
  // One owner per existing upstream request identity. Facts survive revocation;
2
3
  // retaining them never reconstitutes a call capability or a live operation.
3
4
  import { Context, Exit, Scope } from "effect";
@@ -71,7 +72,7 @@ export function requestReads(identity) {
71
72
  export function ownerReads(owner) {
72
73
  const retained = owner.retained;
73
74
  if (!retained.reads) {
74
- retained.reads = new RequestReads();
75
+ retained.reads = new RequestReads(traceFor(owner.identity));
75
76
  if (!owner.open)
76
77
  retained.reads.close();
77
78
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The fixed sentence that marks downstream guidance as third-party. A prefix has
3
+ * no closing delimiter to forge, and connector IDs are deployment-authored.
4
+ */
5
+ export function downstreamOrigin(connectorId) {
6
+ return `Third-party guidance from connector ${JSON.stringify(connectorId.slice(0, 128))}.`;
7
+ }
8
+ /** Downstream description text on one line, so it cannot begin a new listing entry. */
9
+ export function labeledDownstreamDescription(connectorId, description) {
10
+ return `${downstreamOrigin(connectorId)} ${description.replace(/\s+/g, " ").trim()}`;
11
+ }
12
+ const SKILL_RESOURCE = /^skill:\/\/|\/SKILL\.md(?:[?#]|$)/i;
13
+ /** Whether a downstream resource URI addresses skill instructions rather than ordinary data. */
14
+ export function isSkillResource(uri) {
15
+ return SKILL_RESOURCE.test(uri);
16
+ }