@zackbart/connecta 0.24.4 → 0.26.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 (155) hide show
  1. package/AGENTS.md +43 -12
  2. package/CHANGELOG.md +524 -0
  3. package/README.md +32 -15
  4. package/SECURITY.md +6 -5
  5. package/bin/connecta.mjs +5 -1
  6. package/dist/activity.d.ts +10 -3
  7. package/dist/activity.js +10 -1
  8. package/dist/advertised-schema.js +53 -0
  9. package/dist/artifacts/connector.d.ts +55 -0
  10. package/dist/artifacts/connector.js +724 -0
  11. package/dist/artifacts/document.js +76 -0
  12. package/dist/artifacts/guide.js +140 -0
  13. package/dist/artifacts/html-scan.js +329 -0
  14. package/dist/artifacts/json.js +68 -0
  15. package/dist/artifacts/kv-store.d.ts +20 -0
  16. package/dist/artifacts/kv-store.js +140 -0
  17. package/dist/artifacts/markdown.js +449 -0
  18. package/dist/artifacts/module.d.ts +37 -0
  19. package/dist/artifacts/module.js +60 -0
  20. package/dist/artifacts/operations.d.ts +195 -0
  21. package/dist/artifacts/operations.js +944 -0
  22. package/dist/artifacts/refresh.d.ts +53 -0
  23. package/dist/artifacts/refresh.js +235 -0
  24. package/dist/artifacts/routes.js +194 -0
  25. package/dist/artifacts/types.d.ts +214 -0
  26. package/dist/artifacts/types.js +22 -0
  27. package/dist/artifacts/validate.d.ts +62 -0
  28. package/dist/artifacts/validate.js +652 -0
  29. package/dist/artifacts.d.ts +6 -0
  30. package/dist/artifacts.js +9 -0
  31. package/dist/auth/downstream-oauth.js +490 -106
  32. package/dist/call-admission.js +139 -113
  33. package/dist/catalog-service.js +152 -106
  34. package/dist/catalog.js +297 -38
  35. package/dist/concurrency.js +24 -23
  36. package/dist/connector-access.d.ts +18 -4
  37. package/dist/connector-access.js +65 -20
  38. package/dist/connector-caller.d.ts +8 -0
  39. package/dist/connector-caller.js +19 -0
  40. package/dist/connector-scope.js +6 -37
  41. package/dist/connectors/api.d.ts +2 -2
  42. package/dist/connectors/remote-mcp.d.ts +6 -5
  43. package/dist/connectors/remote-mcp.js +330 -240
  44. package/dist/credential-contract.d.ts +9 -0
  45. package/dist/credential-rules.js +2 -2
  46. package/dist/credentials.d.ts +6 -1
  47. package/dist/credentials.js +45 -3
  48. package/dist/errors.d.ts +16 -1
  49. package/dist/errors.js +28 -0
  50. package/dist/execute.js +745 -282
  51. package/dist/executor-admission.d.ts +11 -1
  52. package/dist/executor-admission.js +145 -63
  53. package/dist/executors/quickjs-runtime.js +4 -2
  54. package/dist/executors/quickjs.js +255 -212
  55. package/dist/fix-prompt.js +37 -0
  56. package/dist/index.d.ts +72 -7
  57. package/dist/index.js +281 -26
  58. package/dist/invocation.js +371 -286
  59. package/dist/meta-tools.js +213 -94
  60. package/dist/module-contracts.d.ts +21 -1
  61. package/dist/oauth-callback-outcome.js +94 -0
  62. package/dist/oauth-sealing.js +29 -0
  63. package/dist/operator-ui/fix-prompts.js +87 -0
  64. package/dist/operator-ui/generated.js +2 -2
  65. package/dist/operator-ui/model.d.ts +103 -1
  66. package/dist/operator-ui/setup-commands.js +53 -0
  67. package/dist/operator-ui/view.js +128 -3
  68. package/dist/program-source.js +321 -0
  69. package/dist/providers/notion.js +2 -2
  70. package/dist/providers/revenuecat.js +32 -5
  71. package/dist/providers/stripe.d.ts +3 -2
  72. package/dist/providers/vercel.js +1 -1
  73. package/dist/registry.d.ts +43 -7
  74. package/dist/registry.js +434 -288
  75. package/dist/resumable.js +1291 -0
  76. package/dist/routes/activity.js +74 -154
  77. package/dist/routes/actor-labels.js +114 -0
  78. package/dist/routes/credentials.js +196 -163
  79. package/dist/routes/mcp.js +228 -112
  80. package/dist/routes/oauth-management.js +73 -71
  81. package/dist/routes/oauth.js +44 -16
  82. package/dist/routes/operator.js +83 -0
  83. package/dist/routes/shared.d.ts +25 -1
  84. package/dist/routes/shared.js +20 -2
  85. package/dist/routes/ui-data.js +211 -0
  86. package/dist/routes/ui.js +76 -35
  87. package/dist/run-journal.js +347 -0
  88. package/dist/runtime/admission.js +43 -0
  89. package/dist/runtime/call-admission.js +67 -0
  90. package/dist/runtime/connector-scope.js +72 -0
  91. package/dist/runtime/run.js +174 -0
  92. package/dist/runtime/services.js +130 -0
  93. package/dist/runtime/shared-read.js +106 -0
  94. package/dist/runtime/storage.js +66 -0
  95. package/dist/server.js +137 -89
  96. package/dist/skills.js +28 -9
  97. package/dist/storage/file.js +39 -2
  98. package/dist/storage/memory.js +28 -16
  99. package/dist/timeout.js +39 -43
  100. package/dist/tool-safety.d.ts +27 -1
  101. package/dist/tool-safety.js +31 -0
  102. package/dist/types.d.ts +45 -7
  103. package/dist/ui.d.ts +25 -4
  104. package/dist/ui.js +77 -167
  105. package/dist/url-safety.js +157 -0
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/documentation/architecture.md +367 -16
  109. package/documentation/auth.md +163 -2
  110. package/documentation/code-mode.md +274 -56
  111. package/documentation/meta-tools.md +200 -44
  112. package/ethos.md +46 -47
  113. package/examples/worker/AGENTS.md +6 -0
  114. package/examples/worker/README.md +224 -14
  115. package/examples/worker/src/d1-activity-row.ts +14 -2
  116. package/examples/worker/src/d1-activity.ts +4 -2
  117. package/examples/worker/src/d1-storage.ts +116 -0
  118. package/examples/worker/src/index.ts +42 -7
  119. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  120. package/examples/worker/wrangler.jsonc +21 -5
  121. package/package.json +16 -4
  122. package/templates/node/AGENTS.md +9 -2
  123. package/templates/node/README.md +31 -3
  124. package/templates/node/package.json +1 -1
  125. package/templates/node/src/index.ts +27 -0
  126. package/dist/auth/downstream-oauth.d.ts +0 -200
  127. package/dist/catalog-fingerprint.d.ts +0 -19
  128. package/dist/catalog-limits.d.ts +0 -9
  129. package/dist/catalog-service.d.ts +0 -263
  130. package/dist/catalog.d.ts +0 -87
  131. package/dist/concurrency.d.ts +0 -3
  132. package/dist/connectors/guarded-fetch.d.ts +0 -96
  133. package/dist/credential-rules.d.ts +0 -85
  134. package/dist/execute.d.ts +0 -154
  135. package/dist/executor-result.d.ts +0 -10
  136. package/dist/executors/quickjs-child.d.ts +0 -1
  137. package/dist/executors/quickjs-protocol.d.ts +0 -67
  138. package/dist/favicon.d.ts +0 -1
  139. package/dist/identity.d.ts +0 -4
  140. package/dist/invocation.d.ts +0 -74
  141. package/dist/mcp-result.d.ts +0 -8
  142. package/dist/meta-tools.d.ts +0 -129
  143. package/dist/operator-ui/generated.d.ts +0 -2
  144. package/dist/operator-ui/view.d.ts +0 -188
  145. package/dist/result-shapes.d.ts +0 -13
  146. package/dist/retired-toolkits.d.ts +0 -2
  147. package/dist/routes/activity.d.ts +0 -2
  148. package/dist/routes/credentials.d.ts +0 -2
  149. package/dist/routes/mcp.d.ts +0 -10
  150. package/dist/routes/oauth-management.d.ts +0 -2
  151. package/dist/routes/oauth.d.ts +0 -2
  152. package/dist/routes/ui.d.ts +0 -2
  153. package/dist/server.d.ts +0 -9
  154. package/dist/skills.d.ts +0 -44
  155. package/dist/timeout.d.ts +0 -21
package/AGENTS.md CHANGED
@@ -4,16 +4,21 @@ This is the canonical instruction file for coding agents. `CLAUDE.md` is a
4
4
  symlink to this file so every agent works from the same conventions.
5
5
 
6
6
  A single MCP endpoint aggregating downstream connectors (remote MCP servers and
7
- plain HTTP APIs) behind seven meta-tools, `execute_code` among them. Every
7
+ plain HTTP APIs) behind eight meta-tools, `execute_code` among them. Every
8
8
  deployment configures an executor, and agents reach connectors by writing
9
- JavaScript against it. One fetch-native core, running on both Node and
10
- Cloudflare Workers.
9
+ JavaScript against it. One fetch-native core on Effect behind a Promise API,
10
+ running on both Node and Cloudflare Workers. A program's write pauses until
11
+ `resume_execution` repeats it. One `planned` decision changes that picture — an
12
+ artifacts module — and until it ships, the code and the guides describe what
13
+ exists.
11
14
 
12
15
  - **[`ethos.md`](./ethos.md) is the constitution.** It states what connecta is
13
16
  and isn't, and its decisions table carries a verdict for every shape already
14
- considered — refused, removed, provisional, or gated. Check the table before
15
- designing or building anything: a `refused` row is a "no" with the reason
16
- attached, and a `removed` row (toolkits
17
+ considered — accepted, planned, refused, removed, provisional, or gated.
18
+ Check the table before designing or building anything: a `planned` row is a
19
+ "yes" that has not shipped, binding on review now and absent from the guides
20
+ until it lands, so never document it there as if it exists; a `refused` row
21
+ is a "no" with the reason attached, and a `removed` row (toolkits
17
22
  [#178](https://github.com/zackbart/connecta/issues/178), proactive credential
18
23
  liveness [#179](https://github.com/zackbart/connecta/issues/179), the classic
19
24
  executor-free surface
@@ -30,8 +35,12 @@ Cloudflare Workers.
30
35
 
31
36
  `npm run check` must pass before you claim anything is done — `check:docs` →
32
37
  `check:operator-ui` → `check:lint` → `check:unused` → `typecheck` → `test`
33
- (both vitest projects) → `build` → `check:examples`. It is also the `prepack`
34
- hook. `npm run release:check` adds `check:security` and `check:package` and is
38
+ (both vitest projects) → `test:browser` (the operator UI in Chromium; run
39
+ `npm run test:browser:install` once per machine, since `check` is also what
40
+ `npm pack` runs) → `build` (which prunes unreachable declarations) →
41
+ `check:declarations` (no Effect type in what ships) → `check:bundle` (per-entry
42
+ gzip caps in `scripts/bundle-budget.json`) → `check:examples`. It is also the
43
+ `prepack` hook. `npm run release:check` adds `check:security` and `check:package` and is
35
44
  what CI runs on every pull request, on `main`, and again on publish; use it
36
45
  when touching packaging, dependencies, or exports.
37
46
 
@@ -40,7 +49,7 @@ when touching packaging, dependencies, or exports.
40
49
  - [`ethos.md`](./ethos.md) — what connecta is, what it refuses to be, the
41
50
  decisions table, and the invariants every change must preserve. Check it
42
51
  before building something new; "we already decided not to" is a real answer
43
- there, and its removed/provisional verdicts override anything staler.
52
+ there, and its planned/removed/provisional verdicts override anything staler.
44
53
  - [`documentation/`](./documentation/) — four guides for agents working on the
45
54
  repo: `architecture.md`, `meta-tools.md`, `code-mode.md`, and `auth.md`.
46
55
  Everything else — connectors, providers, admission, storage, the operator
@@ -65,7 +74,7 @@ pins the generated deployment to the CLI package's exact version, restores the
65
74
  template `.gitignore` and the `CLAUDE.md` symlink, and refuses to merge into an
66
75
  existing path.
67
76
  `connecta doctor` verifies a running deployment's health, executor, and exact
68
- seven-tool surface. The template carries its own `Dockerfile` and
77
+ eight-tool surface. The template carries its own `Dockerfile` and
69
78
  `docker-compose.yml`, so the generated project is the container: setup changes
70
79
  must keep the root README, the template (source, container files, and README),
71
80
  and the `scripts/check-package.mjs` smoke — which builds and runs that
@@ -82,7 +91,11 @@ Two boundaries CI enforces that are not obvious from reading a file:
82
91
  (`src/executors/quickjs.ts` + child) are the Node-touching paths and must stay
83
92
  unreachable from the root entry. `test/purity.test.ts` walks the import graph
84
93
  and fails otherwise. Need a Node API? It goes behind an explicit Node-only
85
- subpath (`/node` or `/quickjs`), never the root.
94
+ subpath (`/node` or `/quickjs`), never the root. `effect/testing` is banned
95
+ from `src/` on the same terms — its test clocks and layers have no business
96
+ in a runtime graph — and the purity walk catches it. The same walk keeps
97
+ the root entry to `effect` itself and lets only `src/runtime/run.ts` start
98
+ a fiber.
86
99
  - **Optional modules.** Core owns catalog discovery, execution, invocation, and
87
100
  enforcement. UI, activity history, encrypted credentials, and bearer auth
88
101
  implementations stay behind explicit subpaths and outside the root import
@@ -101,7 +114,9 @@ Two boundaries CI enforces that are not obvious from reading a file:
101
114
  without a subpath because a Worker deployment imports it directly. None may
102
115
  become a dependency or install with core. Enforced by `test/package-surface.test.ts` and
103
116
  `scripts/check-package.mjs`. Anything heavyweight or platform-bound gets a
104
- subpath and an optional peer.
117
+ subpath and an optional peer. The Effect core is the one deliberate
118
+ exception: it is the core, so it is a hard dependency rather than a peer,
119
+ under the rule in [Conventions](#conventions).
105
120
 
106
121
  ## Where new tests go
107
122
 
@@ -118,6 +133,22 @@ an unclassified, double-classified, stale, or reasonless entry.
118
133
  only; it does not enforce style. `npm run check:unused` runs Knip's
119
134
  unused-export and dependency gate. Keep both clean, and prefer removing dead
120
135
  declarations over suppressing a finding.
136
+ - **Effect inside, Promises at the edge.** The core runs on Effect v4 and
137
+ exports none of it; [Effect inside](./documentation/architecture.md#effect-inside)
138
+ has the shell/core shape, the one runner, and the cross-request rules.
139
+ `createConnecta`,
140
+ `remoteMcp()`, `api()`, custom connectors, and every published `.d.ts` stay
141
+ Promise-based and name no Effect type: a deployment author should never need
142
+ a second async paradigm to write a connector, so convert at the boundary,
143
+ not in the caller. Effect is a hard dependency pinned to one exact version,
144
+ never a range, and it is the version Alchemy pins, so a deployment that uses
145
+ both resolves one Effect rather than two. `effect/unstable/*` modules are
146
+ allowed, which is exactly why the pin is exact: they break in minor releases,
147
+ so an upgrade is its own deliberate pull request, never a drive-by in another
148
+ change. `effect/testing` stays out of `src/` (see import-graph purity). The
149
+ MCP edges do not move with the core — `@modelcontextprotocol/server` upward,
150
+ the SDK client downstream; Effect's `McpServer` is parked until the Effect
151
+ core is stable and those edges are re-proven.
121
152
  - **Style.** There is no formatter. Match the surrounding code. The docs voice
122
153
  is precise, occasionally wry, and always explains *why* — don't flatten it
123
154
  into boilerplate.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,530 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## Unreleased
6
+
7
+ ## 0.26.0 — 2026-09-25
8
+
9
+ Programs can now write with host approval. When a program reaches a tool that
10
+ is not explicitly read-only, it pauses before the call and returns the exact
11
+ address, arguments, and a token. The new eighth MCP tool,
12
+ `resume_execution`, repeats that call to show the host the real write and
13
+ replays the program from its journal without resending an uncertain outcome.
14
+ `tools/list` now has eight tools on every deployment, so client allowlists and
15
+ `connecta doctor` installations pinned to 0.25 need updating. Resumable
16
+ writes turn on by default when storage supports `compareAndSet` (memory, file,
17
+ or the Worker example's D1 adapter); a program write that previously failed
18
+ now pauses. Cloudflare KV cannot make that claim atomically, so resumable
19
+ writes stay off there. Set `execute.resumableWrites: false` to silence its
20
+ startup warning. Program clocks and randomness are pinned for replay.
21
+
22
+ Artifacts arrive as an optional module with versioned JSON documents, a
23
+ sandboxed viewer and library, and scheduled refresh of one data document from
24
+ a read-only program. A deployment supplies a Node timer or Worker cron;
25
+ connecta starts no background job. Stored pages use the operator sign-in,
26
+ and a dedicated artifact origin may require signing in again because browser
27
+ storage belongs to one origin. Page scripts run with an opaque origin and no
28
+ network by default; only explicitly allowlisted script, style, and font
29
+ origins may load. Artifact version and configuration writes are approval-exempt inside
30
+ programs by default; deployment config can restore the approval prompt.
31
+ `run_refresh` requires host approval and is excluded from that exemption.
32
+ Deployments that omit the module need no
33
+ artifact storage, R2 bucket, cron, browser binding, or second domain.
34
+
35
+ Before upgrading a Worker deployment that uses D1 activity, add the
36
+ `approval` column shown below. Every admitted `/mcp` request, on Node and
37
+ Workers, has a five-minute total lifetime by default; set
38
+ `admission.requests.maxDurationMs` above any longer authorized request. The
39
+ Worker example also enables `enable_request_signal` for live disconnects.
40
+ To add artifacts to that example, use CAS-capable D1
41
+ storage, optionally R2 for bodies, and explicitly configure any CDN origins.
42
+ The Node template now pins 0.26.0 and shows the optional artifact timer.
43
+ `ConnectorCallErrorCode` gains `conflict` for stale artifact edits, while an
44
+ identity can opt into reviewed exact tool grants that remain visible only while
45
+ the loaded catalog classifies them read-only. The operator UI no longer
46
+ returns downstream OAuth or credential-test text in notices; callers that
47
+ read those route messages must use the fixed outcome and consult server logs
48
+ for diagnostics.
49
+
50
+ ### Added
51
+
52
+ - **Guarded read-only identity grants (#601).** A deployment may put
53
+ `{ tool: "connector.tool", requireReadOnly: true }` in
54
+ `identity.connectorAccess`. The reviewed exact address stays visible only
55
+ while its loaded catalog explicitly says read-only without a contradictory
56
+ destructive hint. A reclassified tool disappears from discovery and every
57
+ call path, including `call_destructive_tool` and approval-exempt programs;
58
+ new names are never added automatically. Existing string grants keep their
59
+ additive meaning, and a pool can still only narrow the identity's view.
60
+ A stale catalog or a dishonest downstream annotation remains a trust limit.
61
+
62
+ - **Resumable writes and `resume_execution` (#565).** A program's call to a
63
+ tool that is not explicitly read-only pauses the run before anything is
64
+ sent and returns `{ paused: { address, args, token, expiresAt, nextAction,
65
+ hint } }`. `resume_execution` — always listed, annotated destructive — must
66
+ repeat the address and args exactly (`approval_mismatch` otherwise, before
67
+ anything is claimed); it claims the run by compare-and-set and replays the
68
+ program from a journal in the caller's result storage, answering every
69
+ earlier host call from its record — no catalog, permit, connector, or
70
+ activity — and sending the approved write once. `approval: "tool"` approves
71
+ the rest of the run's calls to that tool. Each live write is marked on the
72
+ run's header before it is sent, so racing or crashed resumes never send it
73
+ twice. A write sent but never answered — or answered with anything short
74
+ of a refusal or the downstream tool's own error — stops the run as
75
+ `write_outcome_unknown`, even beside a concurrent pause, and is never sent
76
+ again; a program that stops matching its journal fails
77
+ `execution_diverged`; a resumed play that sent writes and then ended without
78
+ pausing (a sandbox failure, a lapsed claim) fails `execution_interrupted`
79
+ rather than replaying reads it never journaled. Every error from a resumed
80
+ play carries `writes` counts, and once a write landed or may have, its
81
+ advice is to check those before re-running rather than a plain re-run; a
82
+ run that sent writes keeps those counts past its deadline. Every paused-run
83
+ storage call has a deadline (the host-call deadline, at least 5 s), so
84
+ storage that stops answering fails the run rather than holding it. A paused
85
+ run survives a restart and expires `execute.pausedRunTtlSeconds` (default 1,800) after its
86
+ first pause; `execute.maxWrites` (default 10) caps a run's writes on top of
87
+ the host-call budget. `execute.resumableWrites` defaults to whether the
88
+ storage has `compareAndSet`; `true` without it refuses to construct.
89
+ `/health` reports `resumableWrites`, and `connecta doctor` expects the eight
90
+ tools and says when resumable writes are off. See code mode "Pausing and
91
+ resuming" (`W1`–`W11`, `X12`).
92
+ - **Config approval exemptions (#566).** `execute.approval` maps connector ids
93
+ and `connector.tool` addresses to `"never"` or `"ask"`: a program calls an
94
+ exempt write without pausing, while it still spends the write budget, is
95
+ journaled, and records activity. The address wins over the connector entry,
96
+ which wins over a connector's own `approval: "never"` default — reserved for
97
+ connectors connecta ships, such as the artifacts connector — so
98
+ `"ask"` switches such a default off. It works with resumable writes off
99
+ too, where an exempt write the program leaves unawaited still finishes
100
+ before the run ends, and one with an unknown outcome is the result. Exempt is never read-only: discovery keeps the tool approval-required
101
+ and marks its row `approval: "exempt"`, `call_tool` still refuses it, and no
102
+ downstream annotation can grant it. An unknown connector id, or an `api()`
103
+ address its tools do not include, refuses to construct. The operator UI's
104
+ per-tool badge gains its third state, "exempt from approval".
105
+ - **Activity for pauses and approvals.** `ActivityOutcome` gains `paused` and
106
+ `approved`, both with zero attempts; `ActivityCallSource` gains
107
+ `resume_execution`; `ToolCallActivityEvent` gains an optional `approval`
108
+ (`"call"` or `"tool"`), set only on an approval. All three are enums — the
109
+ approved arguments are never recorded. The Worker example's D1 activity
110
+ store writes an `approval` column; add it with
111
+ `ALTER TABLE tool_call_activity ADD COLUMN approval TEXT;` before deploying
112
+ the updated adapter. The operator activity page labels both outcomes and
113
+ says what an approval covered.
114
+ - **Artifact storage and validation, at `@zackbart/connecta/artifacts`
115
+ (#562).** `kvArtifactStore(storage, { blobs?, prefix? })` keeps artifacts
116
+ in any `KVStorage` with `compareAndSet` and `list` — one head record per
117
+ artifact, swapped by compare-and-set, with every earlier version immutable
118
+ beside it and bodies content-addressed — and refuses Cloudflare Workers KV
119
+ at construction, because a write that cannot compare-and-set its head can
120
+ lose a teammate's edit. `validateArtifact({ kind, source, documents? })` is
121
+ the static check every save runs: one `id="artifact-root"`, no frames,
122
+ plugins, forms, or `<base>`, external scripts only from explicitly
123
+ allowlisted origins, and no relative or unapproved network resource URLs,
124
+ with a line number and a fix in every message. The Worker example gains
125
+ `r2-artifact-blobs.ts` for keeping bodies
126
+ in R2 beside a D1 store. The subpath adds no dependency and no Effect.
127
+ - **The built-in artifacts connector (#562, #564).** `createConnecta({ artifacts:
128
+ artifacts({ store }) })` appends an `artifacts` connector with five reads —
129
+ `list_artifacts`, `get_artifact`, `get_document`, `validate_artifact`, and
130
+ `get_refresh` — and nine writes: `create_artifact`, `update_artifact`, `patch_artifact` (exact
131
+ find/replace, every edit matching once or nothing changes),
132
+ `set_documents` (plural and atomic), `rollback_artifact`,
133
+ `archive_artifact`, `restore_artifact`, `set_refresh`, and `run_refresh`.
134
+ Version-changing writes name their expected base, and every write records
135
+ its actor from the request's authorization. The connector skips approval, except for `run_refresh`,
136
+ inside `execute_code` through its own `approval:
137
+ "never"` — `execute.approval: { artifacts: "ask" }` turns that off — while
138
+ still spending the write budget and recording activity; `call_tool` still
139
+ refuses it. The connector serves a publishing guide as
140
+ `connector:artifacts`, required before the first write. An optional
141
+ `renderCheck` hook receives the exact frame document and CSP a viewer will
142
+ load and can refuse a write; without one, static validation is the floor.
143
+ The module needs `publicUrl`, and a configured connector named `artifacts`
144
+ refuses to construct.
145
+ - **Artifact library and sandboxed viewer (#563).** `/artifacts` lists pages;
146
+ `/artifacts/<id>` opens one after the same inbound sign-in as the operator
147
+ UI. A viewer snapshot pins exact view and document versions. Page HTML runs
148
+ in an opaque-origin frame with no fetch or worker access. A deployment may
149
+ allow exact CDN origins for scripts, styles, and fonts; none are allowed by
150
+ default. With `artifactOrigin`, the main host redirects artifact paths and
151
+ the dedicated host serves no MCP or operator routes. A browser bearer stored
152
+ on the main origin must be entered again on the artifact origin; the redirect
153
+ carries no credential.
154
+ - **Scheduled artifact refresh (#564).** `set_refresh` attaches a versioned
155
+ program for one data document on a manual, daily, or weekly schedule;
156
+ `run_refresh` triggers it now, and `get_refresh` reports freshness and a
157
+ bounded run history. The module's `runDue()` is called by the deployment's
158
+ Node timer or Worker cron, never by a core background loop. Runs use only
159
+ shared connectors' explicitly read-only tools within the setter's current
160
+ identity and pool grants, and stop if publishing permission is revoked.
161
+ Connector approval exemptions cannot permit a scheduled write. Runs claim
162
+ the artifact by compare-and-set and validate returned JSON, including any
163
+ configured render check, before publishing a new document version. A failure keeps the last good value and marks the page
164
+ stale without rendering raw failure text in the library or viewer. Each tick
165
+ starts at most ten due pages; the Node template and Worker example show the
166
+ optional wiring.
167
+ - **`conflict` (#562).** A new `ConnectorCallErrorCode` for a write whose base
168
+ someone else already moved past, never retryable as-is, with an optional
169
+ bounded `current` map (at most 20 whole-number entries) on
170
+ `ConnectorCallError` and `CallErrorDetails` saying where things stand. A
171
+ program reads it as `err.details.current`; a top-level call returns it in
172
+ the structured error. A write refused with `conflict` inside a program is a
173
+ known failure (`W9`), not an unknown outcome.
174
+
175
+ ### Changed
176
+
177
+ - **`execute_code` asks for a zero-argument program (#602).** Its guidance now
178
+ shows `async () => { ... }`, with `connecta` supplied as the sandbox global.
179
+ Passing `connecta` as an arrow parameter shadows that global and fails; the
180
+ accepted source forms and execution rules have not changed.
181
+ - **Worker resource setup has an optional Alchemy guide (#567).** It covers
182
+ account resources and adoption while keeping the Worker script, domains,
183
+ Browser Rendering, and cron deployment with Wrangler until Alchemy's
184
+ script-upload path is shown to preserve `ctx.access`. Alchemy is not a
185
+ connecta dependency or a second deployment template.
186
+ - **`execute_code` source intake (#576).** Programs over 64 KiB of UTF-8
187
+ source fail before executor admission. The host recovers one fenced async
188
+ arrow even when prose surrounds it, a default-exported arrow, or a named
189
+ async function declaration, so Node and Workers receive the same program.
190
+ - **Eight tools, and instructions that follow the mode.** `tools/list` adds
191
+ `resume_execution` everywhere. With resumable writes on, the MCP
192
+ instructions say programs may write and pause, the `execute_code`
193
+ description advertises the write budget and drops `safety: "readOnly"` from
194
+ its search example (a program looking for a write would not find it), and
195
+ the `usage` skill gains the pause, resume, and unknown-outcome guidance;
196
+ without them, the instructions and description read as they did in 0.25.
197
+ - **The operator UI's "needs approval" badge reads "asks for approval"**, and
198
+ the tool legend says a program pauses for `resume_execution` while a direct
199
+ call crosses `call_destructive_tool`.
200
+ - **A program's clock and randomness are pinned (code mode `P6`).**
201
+ `Date.now()`, `new Date()`, and `Date()` inside `execute_code` return the
202
+ instant the run started and do not advance, and `Math.random()` is an sfc32
203
+ stream seeded per run from the host's CSPRNG; on a Dynamic Worker,
204
+ `crypto.getRandomValues`, `crypto.randomUUID`, and `performance.now()` follow
205
+ the same pin. The replacements are locked like `Error`. Resumable writes
206
+ (#565) replay a paused program from the top, and a program that branched on
207
+ a moving clock or an unseeded draw could not be replayed; pinning every run
208
+ means none behaves differently for having paused. A program that timed its
209
+ own work now measures zero — `diagnostics: true` measures from the host.
210
+
211
+ ### Fixed
212
+
213
+ - **Request admission recovers after missing Worker cleanup (#595).** An
214
+ admitted `/mcp` request now has a configurable total lifetime, five minutes
215
+ by default, covering auth, tools, and response delivery. Its own timer aborts
216
+ live work, including connector calls. If workerd ends it without running
217
+ JavaScript cleanup, the next request or a queued request's timer reclaims
218
+ only the expired permit. An orphaned queued request cannot strand its next
219
+ permit indefinitely, and late cleanup cannot release a successor's permit.
220
+ The Worker example enables `enable_request_signal` for prompt live disconnect
221
+ cleanup.
222
+
223
+ - **Legacy MCP handshake close (#572).** A client that leaves while the older
224
+ protocol handshake is still initializing no longer leaves the SDK's
225
+ `notifications/initialized` promise rejected without a handler. Successful
226
+ handshakes still send the notification.
227
+
228
+ - **Operator notices no longer render downstream error text**
229
+ ([#568](https://github.com/zackbart/connecta/issues/568)). The OAuth
230
+ connect/disconnect notice and the credential Test result showed whatever the
231
+ server sent back, and that text can come from a token endpoint's error body or
232
+ a provider's refusal, either of which can quote the secret it was sent. Each
233
+ notice now says a fixed sentence chosen by outcome, with the fix prompt for
234
+ that outcome, and the page never renders a route's words there even if an
235
+ older server sends them. The routes log the downstream's text instead — a
236
+ failed OAuth action or credential test at `warn`, a passing test's message at
237
+ `info` — and never record it in activity. A `CredentialTestResult.message` is
238
+ now log-only, and a Test refused because nothing usable is stored carries the
239
+ same `credential_required` or `credential_mismatch` problem the Connections
240
+ page shows.
241
+
242
+ - **Compact schemas group an array's union element.** The `compact` format,
243
+ the default for `search_tools` and `connecta.search`, rendered an array of a
244
+ union as `{ op: "replace" } | { op: "append" }[]`, which reads as one
245
+ operation or an array of the other; Linear `save_issue`'s `patch` was one
246
+ such array. A union, intersection, enum, or type list used as an array
247
+ element, a tuple's rest included, now renders parenthesized, as
248
+ `({ op: "replace" } | { op: "append" })[]`, matching the `typescript`
249
+ format. The renderer records which shapes it rendered at operator level
250
+ rather than rescanning its text, so a pipe in a literal, a constraint, or
251
+ property prose adds no parentheses, and every other shape renders byte for
252
+ byte as before (#569).
253
+
254
+ - **A catalog refresh whose connector ignores abort held every later reader.**
255
+ A refresh flight lived until its connector settled, so a `listTools` that
256
+ ignored its signal kept the flight open until the catalog was invalidated,
257
+ and every reader that joined it timed out in turn while the connector stayed
258
+ unlisted. A flight is now bounded by its owner's deadline, or by the default
259
+ 30-second probe timeout for an owner with none. Past it, readers still
260
+ waiting and readers arriving later make a fresh attempt, and a late result
261
+ from the abandoned flight is neither cached nor persisted, so it cannot
262
+ overwrite the fresh one. Abandonment logs a warning
263
+ ([#570](https://github.com/zackbart/connecta/issues/570)).
264
+ - **A joined catalog read ended on its starter's deadline.** Readers of one
265
+ catalog in a request share one read, and that read ran under whichever
266
+ reader started it. A short-deadline `connecta.call` that started it failed a
267
+ concurrent `connecta.search` with the call's timeout, and a search cancelled
268
+ by its client cancelled a read others were still waiting on. The shared read
269
+ now carries its own signal and the probe timeout. Each reader waits under its
270
+ own deadline and cancellation, one that leaves fails alone, and the read is
271
+ cancelled only when every reader has gone. It is still one downstream read
272
+ per connector per request. The registry's cross-request refresh no longer
273
+ hands its owner's cancellation to joiners, which make a fresh attempt instead
274
+ ([#571](https://github.com/zackbart/connecta/issues/571)).
275
+
276
+ ## 0.25.0 — 2026-09-23
277
+
278
+ The Effect core is the release. Admission, deadlines, invocation,
279
+ `execute_code`, discovery, the registry, remote MCP connections, OAuth refresh,
280
+ and the request pipeline now run on Effect v4 behind the same Promise API:
281
+ connectors, `createConnecta`, and every shipped declaration name no Effect
282
+ type, and no connector code changes. The rewrite paid for itself in lifetimes —
283
+ about a dozen bugs where a permit, a lease, or a wait outlived the request that
284
+ owned it, each listed under Fixed — and the release also seals downstream OAuth
285
+ state, stops treating a dead refresh grant as an outage, and bounds results
286
+ below what clients cut off. What breaks: `effect` is a new hard dependency,
287
+ pinned to exactly `4.0.0-rc.117` (about 53 MB installed, +44 KB gzip on the
288
+ root bundle, about +20 ms of Worker cold start); `calls.maxResultBytes`
289
+ defaults to 24,000 instead of 50,000, and a truncated result now leads with its
290
+ notice; `get_result` pages are a header line plus raw text, with `maxBytes`
291
+ clamped to the cap; with `encryptedCredentialVault()`, OAuth tokens are sealed
292
+ the first time they are read, so rolling back to 0.24 means authorizing every
293
+ OAuth connector again; `/ui/connectors/<id>` no longer returns `message`; the
294
+ OAuth callback page names a classified reason instead of echoing the
295
+ provider's; an OAuth URL a downstream advertises off its configured origin must
296
+ be HTTPS on a public host; and `npm run check` now needs Chromium. A deployment
297
+ can ignore the rest: every new surface is optional or defaulted
298
+ (`KVStorage.compareAndSet`, `CredentialVault.seal`/`open`,
299
+ `execute.watchdogMs`, the `typescript` schema format), no existing option
300
+ changes meaning (the result cap changes only its default), and results stashed
301
+ by 0.24 keep paging until they expire.
302
+
303
+ ### Added
304
+
305
+ - **`KVStorage.compareAndSet(key, expected, next, { ttlSeconds })`.** An
306
+ optional atomic claim: `expected: null` means absent (an expired entry
307
+ counts), `next: null` deletes, and a write takes a TTL exactly as `set` does.
308
+ Memory and file storage provide it — the file store atomically under its
309
+ lock, persisted across reopen, and rolled back when the write cannot be
310
+ persisted — and the namespaced views core hands connectors forward it only
311
+ when the store underneath has it. The Worker example gains a D1-backed
312
+ `KVStorage` with compare-and-set and TTL (`examples/worker/src/d1-storage.ts`,
313
+ its schema in the example README, a commented `STORAGE_DB` binding). The
314
+ Cloudflare KV adapter declares none, because eventually consistent storage
315
+ cannot keep that promise. Nothing requires it; downstream OAuth uses it where
316
+ it exists.
317
+ - **`CredentialVault.seal` / `open`.** Optional members for sealing values that
318
+ are not credentials. `encryptedCredentialVault()` implements them with the
319
+ vault's AES-GCM key, binding the connector id, the owner, and the physical
320
+ storage key (which carries the authorization epoch) into the authenticated
321
+ data, so ciphertext copied to another connector, principal, or epoch does not
322
+ open. A vault without them draws one startup warning per OAuth connector.
323
+ - **`execute.watchdogMs`.** A ceiling on how long connecta waits for an
324
+ executor to settle, enforced outside the sandbox: 120,000 ms by default,
325
+ above both executors' own deadlines, with an invalid value falling back like
326
+ its sibling keys. A run still unsettled at the ceiling ends as a
327
+ non-retryable `executor_failed` calling the sandbox unresponsive, and its
328
+ admission lease is released. See code mode `L3`.
329
+ - **TypeScript signatures in discovery.** `includeSchemas: "typescript"` on
330
+ `search_tools` and `connecta.search`, and `format: "typescript"` on
331
+ `connecta.describe`, replace both schema fields with one `signature` — the
332
+ function `connecta.call` resolves to, such as
333
+ `(args: { team: string }) => Promise<unknown>` — for an agent to read, never
334
+ to run; programs stay JavaScript. It rides the compact renderer's walk and
335
+ budgets, and an observed output opens with `/* observed, not declared */`.
336
+ The default stays `compact`: agents in the eval never chose `typescript`, even
337
+ when nudged.
338
+ - **The operator page says what each tool may do and how to fix what is
339
+ wrong.** Every tool carries the path core enforces for it — "runs in
340
+ programs" for an explicitly read-only tool, "needs approval" for everything
341
+ that crosses `call_destructive_tool`. Every failure the page shows offers
342
+ "Copy fix prompt", fixed text chosen by a server-classified problem and the
343
+ configured connector id, with no parameter an error message, token, or URL
344
+ could travel through. The endpoint block offers Claude Code, Codex, and
345
+ `mcpServers` JSON setup for `/mcp` and for each `/mcp/<pool>` the viewer's
346
+ grant admits; none carries a token.
347
+ - **An eval harness, for contributors.** `eval/` drives a deployment only
348
+ through its public surface: `eval:agent` runs headless Claude Code against six
349
+ stateful fake MCP servers and grades the fakes' final state and call ledger,
350
+ never the agent's prose; `eval:smoke` boots the Node template and the Worker
351
+ example; `eval:perf` measures the root Worker bundle and request latency;
352
+ `eval:report` renders a comparison against the baselines in
353
+ `eval/baselines/`. None of it runs in `npm run check` or ships in the
354
+ tarball.
355
+
356
+ ### Changed
357
+
358
+ - **The core runs on Effect.** Each converted module keeps its exported class
359
+ or function exactly and runs an Effect program behind it; one runner,
360
+ `src/runtime/run.ts`, is the only place a fiber starts, and it rethrows
361
+ connecta's own error classes and a caller's abort reason unchanged.
362
+ `documentation/architecture.md` describes the shape and the two workerd rules
363
+ it had to learn. `effect` is a runtime dependency at one exact version, never
364
+ a range — the version Alchemy pins, so a deployment using both resolves one
365
+ copy — and an upgrade is its own release. The cost, against 0.24.4: the root
366
+ entry 235,346 → 279,526 B gzip, the Worker example 263,949 → 314,336 B, each
367
+ provider about +36 KB, `./ui` +39 KB, `./activity` +32 KB, and about 53 MB
368
+ on disk (`effect` has no dependencies of its own). Effect Schema and
369
+ `HttpApi` were measured and not used; `zod` stays for the meta-tool inputs.
370
+ - **Truncated results lead with their handle, under client cutoffs.** Claude
371
+ Code (2.1.280) swaps any MCP result over 50,000 characters for a spill-file
372
+ pointer and a 2,000-character preview, and past twice
373
+ `MAX_MCP_OUTPUT_TOKENS` rejects it with no content at all. The old layout, a
374
+ 50,000-byte preview with the `get_result` notice at the tail, crossed that
375
+ line every time: agents never saw the handle, and two of three models re-ran
376
+ a billable export three times to look again. A `call_tool` or
377
+ `call_destructive_tool` result over its cap now opens with a one-line JSON
378
+ notice (`resultId`, `totalBytes`, `hint`, `nextAction`), then the preview;
379
+ the default `calls.maxResultBytes` drops to 24,000 bytes so notice plus
380
+ preview stays under 25,000; a lone text block is measured, previewed, and
381
+ paged as its text rather than a JSON-escaped content envelope; and a call not
382
+ explicitly read-only says the write already ran and must not be repeated.
383
+ Per-connector overrides and validation are unchanged, and a deployment whose
384
+ clients take more can raise the cap. A result between 24,000 and 50,000
385
+ bytes that used to arrive whole now pages.
386
+ - **A truncated read offers a program before paging.** The notice on an
387
+ explicitly read-only call now says: to find something specific, repeat the
388
+ read inside `execute_code` with `connecta.call` and filter or search it
389
+ there; to read it in full, page with `get_result`. It used to name paging
390
+ alone, and once the handle was visible the eval's Haiku paged a 185 KB CI
391
+ log 24,000 bytes at a time, skipped ranges or stopped after two pages, and
392
+ named the flaky test near the top in all three trials. With the new hint it
393
+ reduced the log in a program and found the real failure in three of three,
394
+ at about a third of the cost per trial. A write's notice is unchanged, since
395
+ repeating a write is what it forbids, and `nextAction` is still the page
396
+ handle for both.
397
+ - **`get_result` pages are raw text.** A page is one text block: a JSON header
398
+ (`resultId`, `offset`, `bytes`, `totalBytes`, `hasMore`, `nextAction`), a
399
+ newline, then the page as stored, where it used to be
400
+ `{ offset, nextOffset?, totalBytes, text }` with every quote and newline in
401
+ the page escaped a second time. `maxBytes` is an upper bound clamped to the
402
+ inline cap of the call that stashed the result, recorded in the stash entry;
403
+ a larger request is answered, not refused, because agents that could finally
404
+ see the handle asked for 50,000-byte pages and Claude Code rejected every
405
+ answer. Entries stashed before the cap was recorded page at the deployment
406
+ cap.
407
+ - **Connector status text stays in the server log.** A status message can quote
408
+ a downstream error body, and that body can quote the secret it rejected, so
409
+ `/ui/connectors/<id>` no longer returns `message` and the page shows fixed
410
+ copy for a classified `problem` instead. The raw text is logged as
411
+ `[connecta] connector "<id>" operator status <state>: <message>`, at warn for
412
+ failures and info for `auth_required`.
413
+ - **The OAuth callback page names a closed reason** — `denied`,
414
+ `provider_error`, `invalid_callback`, `handoff_failed`, or `exchange_failed` —
415
+ instead of echoing the provider's `error` parameter or the exchange error,
416
+ which goes to the operator log. An expired callback reads as
417
+ `invalid_callback`, so refusals stay indistinguishable.
418
+ - **Faster requests.** Each `/mcp` request builds a fresh `McpServer`, and zod
419
+ re-rendered all seven meta-tool input schemas for every one — about 65% of
420
+ `tools/list` CPU. They are now derived once and memoized; `tools/list`
421
+ bodies are byte-identical. On the eval's Node deployment, p50 against 0.24.4:
422
+ `tools/list` 1.43 → 0.68 ms, `call_tool` 2.95 → 1.34 ms, `execute_code`
423
+ 4.94 → 3.45 ms.
424
+ - **The tarball ships only declarations an export reaches.** The build prunes
425
+ every `.d.ts` file no `exports` target reaches — about thirty,
426
+ `src/runtime/` included — and `npm run check:declarations` fails if a shipped
427
+ declaration names an Effect type or is left orphaned. Nothing importable
428
+ changed; those paths were never a supported import.
429
+ - **`npm run check` needs Chromium.** It runs the operator UI's browser suite
430
+ after vitest (`npm run test:browser:install` once per machine; `npm pack`
431
+ runs `check` too), then `check:declarations` and `check:bundle`, which caps
432
+ every entry's gzip size against `scripts/bundle-budget.json`. CI and publish
433
+ install Chromium first.
434
+ - RevenueCat's product store-state plan family and targeting-rule tools arrive
435
+ classified instead of failing closed; the five tools RevenueCat deprecated
436
+ keep their verdicts, and the usage guide routes store changes through the
437
+ plan workflow (#560).
438
+
439
+ ### Fixed
440
+
441
+ - **Downstream OAuth state at rest.** A `remoteMcp()` OAuth connector's tokens,
442
+ registered client secret, and PKCE verifier were written to storage as
443
+ plaintext JSON beside vault-encrypted credentials. With a vault that has
444
+ `seal`/`open` they are now sealed on write, and plaintext left by an older
445
+ release is resealed where it lies on first read, conditional on nothing
446
+ having changed it meanwhile. A value that does not open reads as absent:
447
+ `auth_required`, with a warning. Without a vault, stored bytes are
448
+ unchanged.
449
+ - **Dead refresh grants.** A refused refresh — any 4xx but 408, 425, or 429,
450
+ or a 2xx carrying an OAuth error such as GitHub's `bad_refresh_token` — was
451
+ reported as a generic failure and the dead token sent again, while an outage
452
+ the SDK could not parse was reported as needing consent. A refusal now ends
453
+ as `auth_required` with the refused tokens deleted, so the next
454
+ `authorize_connector` reaches consent; a 5xx, 408, 425, 429, or network
455
+ failure is a retryable `unavailable` (or `rate_limited`, with `retryAfterMs`
456
+ from `Retry-After`), the grant is kept, and a passive call writes no consent
457
+ URL. Every request joined on the refresh gets the same verdict.
458
+ - **A refused refresh token could be redeemed twice.** A request that owned a
459
+ refresh and was cancelled while deleting the refused grant released its
460
+ joiners early, so a request arriving meanwhile sent the refused token again.
461
+ Deleting it also no longer overwrites a consent that lands between the read
462
+ and the delete, where the store has `compareAndSet`.
463
+ - **OAuth URLs a downstream advertises.** Connecta fetched the authorization
464
+ servers, token endpoints, and registration endpoints a remote server's
465
+ metadata names from the host, so a compromised downstream could aim a Node or
466
+ Docker deployment at `169.254.169.254` or its LAN. A URL off the connector's
467
+ configured origin must now be HTTPS on a host that is not private by syntax,
468
+ and a refused one is never requested; it fails as a non-retryable
469
+ `connector_call_failed` naming only the host. A loopback-configured connector
470
+ may still learn loopback URLs. DNS rebinding is out of scope, as the auth
471
+ guide says.
472
+ - **Admission permits and leases that outlived their caller.**
473
+ - An `/mcp` request that stalled after admission — in inbound auth, a pool
474
+ grant, or a tool handler that ignores cancellation — kept its permit after
475
+ its client left. On Workers, where such a request is cancelled as hung, the
476
+ permit leaked for the isolate's life.
477
+ - `execute_code` awaited its executor with no outer bound. A wedged Dynamic
478
+ Worker never fired its in-isolate deadline, so the run held its code-pool
479
+ lease forever; two of those filled the default pool and every later
480
+ program queued behind them. `execute.watchdogMs` ends that wait.
481
+ - An admitting executor whose `acquire()` ignored the signal held a cancelled
482
+ request until it granted a lease.
483
+ - **Work that outlived a cancelled request.** `connecta.search` and
484
+ `connecta.describe` kept loading catalogs after their run was cancelled, and
485
+ one in flight held the program until its probe deadline; `search_tools`
486
+ probes ran on after their caller left; operator activity pages kept looking
487
+ up labels from the identity provider after the reader had gone; and legacy
488
+ `/mcp` requests never closed their `McpServer`.
489
+ - **Call admission on Workers.** Handing a permit to a queued call read the
490
+ waiter's abort signal from inside the releasing request, which workerd
491
+ refuses: the releasing call reported `connector_call_failed` despite
492
+ succeeding, and the waiter stranded until its deadline.
493
+ - **Remote MCP connections.** A call waiting on a connect could resume after
494
+ teardown, a forced re-authorization, or a credential rotation had cleared the
495
+ client, and fail with "Cannot read properties of null (reading 'callTool')".
496
+ And a transport whose close never settled held the connection's teardown
497
+ forever, hanging direct callers such as the credential Test action; closing a
498
+ session and the transport are now bounded to a second each.
499
+ - **A stale catalog persisted after invalidation.** Evicting a personal
500
+ registry while its catalog refresh was in flight let a replacement for the
501
+ same principal invalidate the catalog, and the evicted refresh then persisted
502
+ the pre-change listing again. Eviction now skips a registry with a refresh or
503
+ catalog write in flight.
504
+ - **QuickJS shutdown.** A child that ignored SIGTERM outlived `close()`, which
505
+ stopped waiting after a second; it now gets SIGKILL. `close()` also waits for
506
+ children recycled before it, not only its own.
507
+ - **Operator routes.** A credential PUT read its whole body before the
508
+ 20,000-character check; reading now stops past 60,000 bytes with the same
509
+ 413. An OAuth disconnect that rejected with no reason reported 204.
510
+ - A program ending in `};` failed on QuickJS with `SyntaxError: expecting ')'`,
511
+ because both executors evaluate a program inside parentheses; on a Dynamic
512
+ Worker it failed the same way or, with a comment after it, quietly returned
513
+ `undefined`. `execute_code` now drops semicolons that trail the arrow
514
+ expression, with whitespace and comments after them, before either executor
515
+ sees the program. Anything else stays as written, so `async () => 1; 2` is
516
+ still more than one expression (code mode `P1`).
517
+ - A scoped `connecta.search` whose probe timed out named `search_tools` in
518
+ `queryAnalysis.catalogError`, a tool a program cannot call.
519
+ - Recovery text for a missing or rejected static credential sent operators and
520
+ agents to a `/credentials` page that no longer exists; it now points at
521
+ `authorize_connector` for recovery options and at the connection in the
522
+ operator UI (#551).
523
+ - Compact schema rendering accumulated every member's text of an oversized
524
+ composite before checking it against the byte budget — megabytes of
525
+ transient text for a wide schema with long descriptions. It now stops
526
+ consuming members once the budget cannot be met; output within budget is
527
+ unchanged (#558).
528
+
5
529
  ## 0.24.4 — 2026-09-17
6
530
 
7
531
  The operator UI is the release. Its connections page is now a summary line and