@zackbart/connecta 0.40.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.
- package/CHANGELOG.md +87 -0
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +30 -4
- package/dist/activity.js +28 -2
- package/dist/agent-observation.js +24 -2
- package/dist/agent-routes.js +4 -41
- package/dist/auth/clerk.d.ts +6 -1
- package/dist/auth/clerk.js +16 -0
- package/dist/auth/cloudflare-access.js +49 -7
- package/dist/auth/downstream-oauth/consent.js +79 -29
- package/dist/auth/downstream-oauth/grant-store.js +58 -7
- package/dist/auth/downstream-oauth/refresh-records.js +6 -5
- package/dist/auth/downstream-oauth.js +12 -4
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +51 -21
- package/dist/catalog-cache.js +62 -21
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-service.js +39 -12
- package/dist/catalog.js +103 -38
- package/dist/config.d.ts +10 -4
- package/dist/config.js +30 -7
- package/dist/connectors/remote-mcp/call.js +1 -1
- package/dist/connectors/remote-mcp/connection.js +52 -18
- package/dist/connectors/remote-mcp/oauth.js +2 -2
- package/dist/connectors/remote-mcp/resources.js +5 -2
- package/dist/connectors/remote-mcp/transport.js +24 -6
- package/dist/connectors/remote-mcp.js +4 -4
- package/dist/credential-contract.d.ts +3 -1
- package/dist/describe-config.d.ts +1 -0
- package/dist/describe-config.js +1 -0
- package/dist/downstream-input.js +11 -2
- package/dist/errors.d.ts +8 -1
- package/dist/errors.js +2 -0
- package/dist/execute.js +20 -7
- package/dist/executor-result.js +1 -1
- package/dist/executors/quickjs-child.js +1 -1
- package/dist/executors/quickjs-runtime.js +1 -1
- package/dist/executors/quickjs.js +1 -1
- package/dist/guarded-transport.js +2 -0
- package/dist/http/client.js +7 -0
- package/dist/http/hatch.js +2 -0
- package/dist/http/node-transport.js +110 -4
- package/dist/http/oauth.js +17 -6
- package/dist/index.js +10 -21
- package/dist/invocation-failure.js +28 -0
- package/dist/invocation.js +72 -33
- package/dist/membership.d.ts +1 -1
- package/dist/meta-tools.js +27 -13
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-tools.js +11 -4
- package/dist/node.d.ts +7 -0
- package/dist/node.js +82 -3
- package/dist/operation-activity.js +103 -0
- package/dist/operator-record.d.ts +8 -0
- package/dist/operator-record.js +15 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/breeze/index.js +1 -1
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/cloudflare/index.js +1 -1
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/forms/index.js +1 -1
- package/dist/providers/github/index.js +1 -1
- package/dist/providers/gmail/index.js +1 -1
- package/dist/providers/granola/index.js +1 -1
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.js +1 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/quickbooks/api.js +28 -25
- package/dist/providers/quickbooks/index.js +1 -1
- package/dist/providers/quickbooks/lib/writes.js +55 -0
- package/dist/providers/quickbooks/safety.js +17 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +1 -1
- package/dist/providers/slides/index.js +1 -1
- package/dist/providers/stripe/index.js +2 -2
- package/dist/providers/tithely/index.js +1 -1
- package/dist/providers/vercel/api.js +1 -0
- package/dist/registry.js +16 -4
- package/dist/resource-read.js +21 -2
- package/dist/route-names.js +44 -0
- package/dist/routes/activity-context.js +1 -0
- package/dist/routes/activity-disclosure.js +26 -4
- package/dist/routes/activity.js +2 -1
- package/dist/routes/mcp.js +32 -13
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui-data.js +3 -2
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +66 -14
- package/dist/runtime/request-scope.js +2 -1
- package/dist/skill-origin.js +16 -0
- package/dist/skills.js +132 -43
- package/dist/storage/d1-driver.js +33 -5
- package/dist/storage/keys.js +7 -1
- package/dist/storage/sql-native.js +60 -3
- package/dist/storage/sql.js +6 -0
- package/dist/timing-trace.js +84 -0
- package/dist/types.d.ts +5 -0
- package/dist/url-safety.js +38 -20
- package/dist/utility-recovery.js +33 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +33 -8
- package/documentation/architecture.md +48 -1
- package/documentation/auth.md +116 -36
- package/documentation/code-mode.md +61 -46
- package/documentation/deploying.md +25 -1
- package/documentation/meta-tools.md +113 -11
- package/documentation/operating.md +86 -1
- package/documentation/operator-ui.md +51 -2
- package/documentation/providers.md +16 -14
- package/package.json +10 -8
- 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.
|
package/dist/routes/activity.js
CHANGED
|
@@ -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 } : {}),
|
package/dist/routes/mcp.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
|
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
|
-
|
|
770
|
-
|
|
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
|
}
|
package/dist/routes/ui-config.js
CHANGED
|
@@ -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
|
|
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
|
package/dist/routes/ui-data.js
CHANGED
|
@@ -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
|
-
...(
|
|
275
|
+
...(observedAgeMs !== undefined ? { observedAgeMs } : {}),
|
|
275
276
|
},
|
|
276
|
-
...(
|
|
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,11 @@ 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,
|
|
28
33
|
accessToken: native.accessToken
|
|
29
34
|
? (hash) => typed(() => (getNativeStorage(edge) ? native.accessToken(hash) : Effect.succeed(undefined)))
|
|
30
35
|
: undefined,
|
|
@@ -37,6 +42,13 @@ export function registerNativeStorage(edge, native) {
|
|
|
37
42
|
: Effect.forEach(keys, (key) => current(edge, "get", own.get, [key], () => native.get(key)))),
|
|
38
43
|
}
|
|
39
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
|
+
: {}),
|
|
40
52
|
set: (key, value, options) => current(edge, "set", own.set, [key, value, options], () => native.set(key, value, options)),
|
|
41
53
|
delete: (key) => current(edge, "delete", own.delete, [key], () => native.delete(key)),
|
|
42
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,7 @@
|
|
|
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";
|
|
39
41
|
const roots = new WeakMap();
|
|
40
42
|
const observed = new WeakMap();
|
|
41
43
|
/**
|
|
@@ -88,7 +90,7 @@ export function observeStorageWrites(storage, layer) {
|
|
|
88
90
|
return storage;
|
|
89
91
|
}
|
|
90
92
|
/** The underlying store and key a read of `key` reaches, when every layer is observed. */
|
|
91
|
-
function
|
|
93
|
+
export function physicalStorageAddress(storage, key) {
|
|
92
94
|
const entry = observed.get(storage);
|
|
93
95
|
if (!entry)
|
|
94
96
|
return undefined;
|
|
@@ -99,10 +101,17 @@ function physical(storage, key) {
|
|
|
99
101
|
own.list !== storage.list ||
|
|
100
102
|
own.compareAndSet !== storage.compareAndSet)
|
|
101
103
|
return undefined;
|
|
102
|
-
return "root" in layer ? { store: layer.root, key } :
|
|
104
|
+
return "root" in layer ? { store: layer.root, key } : physicalStorageAddress(layer.storage, layer.prefix + key);
|
|
103
105
|
}
|
|
104
106
|
const weakReferences = () => typeof WeakRef === "function" && typeof FinalizationRegistry === "function";
|
|
105
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
|
+
}
|
|
106
115
|
closed = false;
|
|
107
116
|
stores = new Map();
|
|
108
117
|
ref = weakReferences() ? new WeakRef(this) : undefined;
|
|
@@ -129,7 +138,7 @@ export class RequestReads {
|
|
|
129
138
|
}
|
|
130
139
|
/** A memoized answer, or how to record the value a storage read returns. */
|
|
131
140
|
lookup(storage, key, fresh) {
|
|
132
|
-
const address = this.closed || !this.ref ? undefined :
|
|
141
|
+
const address = this.closed || !this.ref ? undefined : physicalStorageAddress(storage, key);
|
|
133
142
|
if (!address)
|
|
134
143
|
return undefined;
|
|
135
144
|
const entries = this.entriesFor(address.store);
|
|
@@ -155,31 +164,70 @@ export class RequestReads {
|
|
|
155
164
|
read(storage, key, load, fresh = false) {
|
|
156
165
|
return Effect.suspend(() => {
|
|
157
166
|
const found = this.lookup(storage, key, fresh);
|
|
158
|
-
if (!found)
|
|
167
|
+
if (!found && !this.trace)
|
|
159
168
|
return load;
|
|
160
|
-
|
|
169
|
+
const started = this.trace ? performance.now() : 0;
|
|
170
|
+
if (found?.hit) {
|
|
171
|
+
if (this.trace)
|
|
172
|
+
this.note(fresh, true, found.value, started);
|
|
161
173
|
return Effect.succeed(found.value);
|
|
162
|
-
|
|
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
|
+
}));
|
|
163
180
|
});
|
|
164
181
|
}
|
|
165
182
|
/** Prefetch only keys whose callers already permit request-local staleness. */
|
|
166
|
-
prefetch(requests, load) {
|
|
183
|
+
prefetch(requests, load, linked) {
|
|
167
184
|
return Effect.suspend(() => {
|
|
168
185
|
const pending = requests.map(({ storage, key }) => ({
|
|
169
|
-
address:
|
|
186
|
+
address: physicalStorageAddress(storage, key),
|
|
170
187
|
found: this.lookup(storage, key, false),
|
|
171
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;
|
|
172
194
|
const root = pending[0]?.address?.store;
|
|
173
195
|
const storage = root && roots.get(root);
|
|
174
196
|
if (!storage || pending.some(({ address, found }) => !found || address?.store !== root))
|
|
175
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
|
+
}
|
|
176
220
|
const misses = pending.filter(({ found }) => found && !found.hit);
|
|
177
221
|
if (!misses.length)
|
|
178
222
|
return Effect.void;
|
|
179
223
|
return Effect.map(load(storage, misses.map(({ address }) => address.key)), (values) => {
|
|
180
224
|
misses.forEach(({ found }, i) => {
|
|
181
|
-
if (found && !found.hit)
|
|
182
|
-
|
|
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
|
+
}
|
|
183
231
|
});
|
|
184
232
|
});
|
|
185
233
|
});
|
|
@@ -187,12 +235,16 @@ export class RequestReads {
|
|
|
187
235
|
/** `read` for a Promise-shaped caller, without another Effect edge. */
|
|
188
236
|
async readPromise(storage, key, load, fresh = false) {
|
|
189
237
|
const found = this.lookup(storage, key, fresh);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
238
|
+
const started = this.trace ? performance.now() : 0;
|
|
239
|
+
if (found?.hit) {
|
|
240
|
+
if (this.trace)
|
|
241
|
+
this.note(fresh, true, found.value, started);
|
|
193
242
|
return found.value;
|
|
243
|
+
}
|
|
194
244
|
const value = await load();
|
|
195
|
-
found
|
|
245
|
+
found?.record(value);
|
|
246
|
+
if (this.trace)
|
|
247
|
+
this.note(fresh, false, value, started);
|
|
196
248
|
return value;
|
|
197
249
|
}
|
|
198
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
|
+
}
|