@zackbart/connecta 0.40.0 → 0.42.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 (195) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/bin/connecta.mjs +26 -17
  3. package/bin/meta-tool-names.d.mts +3 -1
  4. package/bin/meta-tool-names.mjs +4 -1
  5. package/bin/version.mjs +1 -1
  6. package/dist/activity.d.ts +32 -5
  7. package/dist/activity.js +32 -6
  8. package/dist/advertised-schema.js +15 -0
  9. package/dist/agent-observation.js +24 -2
  10. package/dist/agent-routes.js +33 -44
  11. package/dist/auth/clerk.d.ts +6 -1
  12. package/dist/auth/clerk.js +16 -0
  13. package/dist/auth/cloudflare-access.js +49 -7
  14. package/dist/auth/downstream-oauth/consent.js +79 -29
  15. package/dist/auth/downstream-oauth/grant-store.js +58 -7
  16. package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
  17. package/dist/auth/downstream-oauth/refresh-records.js +6 -5
  18. package/dist/auth/downstream-oauth.js +15 -4
  19. package/dist/auth/oauth-request-not-sent.js +4 -0
  20. package/dist/auth/static-oauth.js +77 -39
  21. package/dist/auth-elicitation.js +118 -61
  22. package/dist/authorization-handoff.js +36 -3
  23. package/dist/catalog-cache.js +352 -43
  24. package/dist/catalog-freshness.js +25 -0
  25. package/dist/catalog-service.js +408 -78
  26. package/dist/catalog.js +216 -62
  27. package/dist/config-defaults.d.ts +2 -1
  28. package/dist/config-defaults.js +4 -1
  29. package/dist/config.d.ts +53 -21
  30. package/dist/config.js +56 -17
  31. package/dist/connectors/remote-mcp/call.js +1 -1
  32. package/dist/connectors/remote-mcp/catalog.js +52 -28
  33. package/dist/connectors/remote-mcp/connection.js +113 -22
  34. package/dist/connectors/remote-mcp/oauth.js +2 -2
  35. package/dist/connectors/remote-mcp/resources.js +5 -2
  36. package/dist/connectors/remote-mcp/transport.js +24 -6
  37. package/dist/connectors/remote-mcp.js +14 -5
  38. package/dist/credential-contract.d.ts +3 -1
  39. package/dist/describe-config.d.ts +4 -2
  40. package/dist/describe-config.js +1 -0
  41. package/dist/discovery-health.js +26 -11
  42. package/dist/downstream-input.js +11 -2
  43. package/dist/errors.d.ts +60 -4
  44. package/dist/errors.js +88 -3
  45. package/dist/execute.js +491 -202
  46. package/dist/executor-admission.js +2 -0
  47. package/dist/executor-contract.js +6 -0
  48. package/dist/executor-result.js +3 -3
  49. package/dist/executors/quickjs-child.js +14 -5
  50. package/dist/executors/quickjs-runtime.d.ts +2 -0
  51. package/dist/executors/quickjs-runtime.js +37 -20
  52. package/dist/executors/quickjs.js +24 -7
  53. package/dist/guarded-transport.js +2 -0
  54. package/dist/guest-functions.js +5 -4
  55. package/dist/guest-runtime.js +37 -1
  56. package/dist/guest-types.d.ts +18 -0
  57. package/dist/http/client.js +14 -2
  58. package/dist/http/errors.js +9 -4
  59. package/dist/http/fields.js +17 -5
  60. package/dist/http/hatch.js +11 -3
  61. package/dist/http/node-transport.js +110 -4
  62. package/dist/http/oauth.js +17 -6
  63. package/dist/http/paging.js +1 -1
  64. package/dist/http/provider.js +2 -6
  65. package/dist/http/vendor.js +6 -1
  66. package/dist/index.d.ts +1 -1
  67. package/dist/index.js +15 -21
  68. package/dist/invocation-failure.js +28 -0
  69. package/dist/invocation.js +167 -64
  70. package/dist/membership.d.ts +1 -1
  71. package/dist/meta-output.js +24 -18
  72. package/dist/meta-tool-names.d.ts +1 -1
  73. package/dist/meta-tool-names.js +1 -1
  74. package/dist/meta-tools.js +286 -73
  75. package/dist/module-contracts.d.ts +3 -1
  76. package/dist/native-recovery.js +4 -1
  77. package/dist/native-tools.js +177 -55
  78. package/dist/node.d.ts +7 -0
  79. package/dist/node.js +85 -4
  80. package/dist/oauth-handoff.js +53 -43
  81. package/dist/operation-activity.js +104 -0
  82. package/dist/operation-vocabulary.js +6 -0
  83. package/dist/operator-record.d.ts +9 -1
  84. package/dist/operator-record.js +16 -0
  85. package/dist/operator-ui/generated.js +2 -2
  86. package/dist/program-result.js +2 -0
  87. package/dist/program-source.js +1 -1
  88. package/dist/program-writes.js +112 -19
  89. package/dist/provider.d.ts +1 -14
  90. package/dist/provider.js +0 -3
  91. package/dist/providers/_shared/google/kit.js +2 -2
  92. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  93. package/dist/providers/breeze/api.js +2 -2
  94. package/dist/providers/breeze/index.js +1 -1
  95. package/dist/providers/breeze/skill.generated.js +1 -1
  96. package/dist/providers/ccb/api.js +2 -2
  97. package/dist/providers/ccb/index.js +1 -1
  98. package/dist/providers/ccb/skill.generated.js +1 -1
  99. package/dist/providers/cloudflare/api.js +2 -2
  100. package/dist/providers/cloudflare/index.js +3 -1
  101. package/dist/providers/cloudflare/skill.generated.js +1 -1
  102. package/dist/providers/docs/index.js +1 -1
  103. package/dist/providers/docs/skill.generated.js +1 -1
  104. package/dist/providers/drive/index.js +1 -1
  105. package/dist/providers/drive/skill.generated.js +1 -1
  106. package/dist/providers/forms/index.js +2 -1
  107. package/dist/providers/forms/skill.generated.js +1 -1
  108. package/dist/providers/github/api.js +98 -6
  109. package/dist/providers/github/index.js +8 -3
  110. package/dist/providers/github/lib/auth.d.ts +3 -1
  111. package/dist/providers/github/lib/auth.js +43 -12
  112. package/dist/providers/github/lib/discovery.js +11 -1
  113. package/dist/providers/github/lib/operations.js +8 -0
  114. package/dist/providers/github/safety.js +39 -0
  115. package/dist/providers/github/skill.generated.js +1 -1
  116. package/dist/providers/github/tools.js +12 -5
  117. package/dist/providers/gmail/index.js +2 -1
  118. package/dist/providers/gmail/skill.generated.js +1 -1
  119. package/dist/providers/granola/index.js +1 -1
  120. package/dist/providers/granola/skill.generated.js +1 -1
  121. package/dist/providers/overflow/index.js +3 -1
  122. package/dist/providers/overflow/skill.generated.js +1 -1
  123. package/dist/providers/planning-center/index.js +12 -6
  124. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  125. package/dist/providers/planning-center/skill.generated.js +1 -1
  126. package/dist/providers/quickbooks/api.js +28 -25
  127. package/dist/providers/quickbooks/index.js +3 -6
  128. package/dist/providers/quickbooks/lib/writes.js +55 -0
  129. package/dist/providers/quickbooks/safety.js +17 -5
  130. package/dist/providers/quickbooks/skill.generated.js +1 -1
  131. package/dist/providers/sheets/index.js +2 -1
  132. package/dist/providers/sheets/skill.generated.js +1 -1
  133. package/dist/providers/slides/index.js +3 -3
  134. package/dist/providers/slides/skill.generated.js +1 -1
  135. package/dist/providers/stripe/index.js +2 -2
  136. package/dist/providers/stripe/skill.generated.js +1 -1
  137. package/dist/providers/tithely/index.js +2 -1
  138. package/dist/providers/tithely/skill.generated.js +1 -1
  139. package/dist/providers/vercel/api.js +3 -2
  140. package/dist/providers/vercel/skill.generated.js +1 -1
  141. package/dist/registry.d.ts +36 -4
  142. package/dist/registry.js +142 -30
  143. package/dist/resource-read.js +21 -2
  144. package/dist/result-preview.js +210 -0
  145. package/dist/result-seal.js +168 -0
  146. package/dist/route-names.js +53 -0
  147. package/dist/routes/activity-context.js +1 -0
  148. package/dist/routes/activity-disclosure.js +26 -4
  149. package/dist/routes/activity.js +2 -1
  150. package/dist/routes/connect.js +21 -13
  151. package/dist/routes/mcp.js +83 -35
  152. package/dist/routes/shared.d.ts +6 -0
  153. package/dist/routes/ui-config.js +1 -1
  154. package/dist/routes/ui-data.js +3 -2
  155. package/dist/runtime/call.js +15 -10
  156. package/dist/runtime/native-storage.js +12 -0
  157. package/dist/runtime/request-reads.js +66 -14
  158. package/dist/runtime/request-scope.js +38 -2
  159. package/dist/runtime/run.js +4 -1
  160. package/dist/sent-secrets.d.ts +20 -0
  161. package/dist/sent-secrets.js +49 -1
  162. package/dist/skill-origin.js +16 -0
  163. package/dist/skills.js +213 -57
  164. package/dist/storage/d1-driver.js +33 -5
  165. package/dist/storage/keys.js +16 -7
  166. package/dist/storage/sql-native.js +60 -3
  167. package/dist/storage/sql.js +6 -0
  168. package/dist/timing-trace.js +84 -0
  169. package/dist/tool-safety.d.ts +33 -0
  170. package/dist/tool-safety.js +51 -15
  171. package/dist/types.d.ts +36 -0
  172. package/dist/url-safety.js +38 -20
  173. package/dist/usage-guide.js +14 -13
  174. package/dist/utility-recovery.js +33 -0
  175. package/dist/validate.js +29 -10
  176. package/dist/version.d.ts +1 -1
  177. package/dist/version.js +1 -1
  178. package/dist/worker.js +60 -15
  179. package/documentation/architecture.md +152 -43
  180. package/documentation/auth.md +160 -53
  181. package/documentation/code-mode.md +396 -119
  182. package/documentation/deploying.md +35 -10
  183. package/documentation/hosted-mcp-migration.md +9 -3
  184. package/documentation/integrating.md +27 -5
  185. package/documentation/meta-tools.md +761 -157
  186. package/documentation/operating.md +91 -4
  187. package/documentation/operator-ui.md +51 -2
  188. package/documentation/providers.md +34 -20
  189. package/examples/worker/README.md +3 -3
  190. package/guides/how-it-works.md +2 -1
  191. package/guides/safety-and-trust.md +1 -3
  192. package/package.json +10 -8
  193. package/templates/node/AGENTS.md +3 -3
  194. package/templates/node/README.md +1 -1
  195. package/templates/node/package.json +1 -1
package/dist/execute.js CHANGED
@@ -1,4 +1,8 @@
1
+ import { operationActivity } from "./operation-activity.js";
2
+ import { SkillLookupError } from "./skills.js";
3
+ import { skillLookupRecovery, utilityRecovery } from "./utility-recovery.js";
1
4
  import { META_ROUTES, routeNames } from "./agent-routes.js";
5
+ import { READ_PROGRAM_SOURCE } from "./tool-safety.js";
2
6
  import { authCall, authFlow, authInvoke, authMethod } from "./auth/native.js";
3
7
  import { nativeRecovery } from "./native-recovery.js";
4
8
  import { GUEST_API_DECLARATION, GUEST_FUNCTIONS, UNCOUNTED_GUEST_FUNCTIONS, } from "./guest-functions.js";
@@ -8,29 +12,38 @@ import { Cause, Deferred, Duration, Effect, Exit } from "effect";
8
12
  import * as z from "zod";
9
13
  import { advertisedSchema } from "./advertised-schema.js";
10
14
  import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, } from "./catalog-service.js";
11
- import { createMetaTools, guardProgramReturn, jsonResult, pageProgramResult, } from "./meta-tools.js";
12
- import { PROGRAM_RESULT_INLINE_BYTES, PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_WIRE_BYTES, } from "./program-result.js";
15
+ import { createMetaTools, guardProgramReturn, jsonResult, pageProgramResult, releaseStashedResult, } from "./meta-tools.js";
16
+ import { PROGRAM_RESULT_INLINE_BYTES, PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_READ_BYTES, PROGRAM_RESULT_WIRE_BYTES, } from "./program-result.js";
13
17
  import { MAX_EXECUTE_LOG_CHARS, MAX_EXECUTE_RESULT_CHARS, truncateExecuteText } from "./executor-result.js";
14
18
  import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor } from "./executor-admission.js";
15
- import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
19
+ import { boundedEchoText, clipSerialized, ConnectorCallError, msg } from "./errors.js";
16
20
  import { DEFAULT_MAX_WRITES, ProgramWrites, writeStateOf } from "./program-writes.js";
17
21
  import { InvocationFailure, InvocationService, timed } from "./invocation.js";
18
22
  import { guestPrelude, programError, wrapGuestProgram } from "./guest-runtime.js";
19
23
  import { EXECUTE_OUTPUT } from "./meta-output.js";
20
24
  import { normalizeProgramSource } from "./program-source.js";
21
25
  import { readResource } from "./resource-read.js";
26
+ import { failureRecord, logFailure } from "./operator-record.js";
27
+ import { HeldResults, sealHeldResults } from "./result-seal.js";
22
28
  import { redactAgentOutput, sentSecretsForRequest } from "./sent-secrets.js";
23
29
  import { normalizeTimeoutMs } from "./timeout.js";
24
30
  import { fromSignal, runEdge, withDeadlineEffect } from "./runtime/run.js";
25
31
  import { closeScope } from "./runtime/connector-scope.js";
26
32
  import { RequestScope, requestOwner } from "./runtime/request-scope.js";
27
33
  import { executorProgram } from "./runtime/executor.js";
28
- import { connectorGuide, connectorGuideRequired, connectorSkillName, hasConnectorGuides, SkillsRegistry, } from "./skills.js";
34
+ import { connectorGuide, connectorGuideRequired, addsInformation, connectorInventoryTitle, inventoryNote, hasConnectorGuides, SkillsRegistry, } from "./skills.js";
29
35
  /** Keep one model-written program from amplifying into an unbounded fan-out. */
30
36
  const EXECUTE_MAX_HOST_CALLS = CONFIG_DEFAULTS.execute.maxHostCalls;
31
37
  const EXECUTE_HOST_CALL_TIMEOUT_MS = CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
32
38
  /** Above every program in the recorded evals, below the QuickJS IPC ceiling. */
33
39
  const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
40
+ /** All of one run's stash reclaim, in milliseconds of waiting on storage. */
41
+ const RECLAIM_BUDGET_MS = 1_000;
42
+ /**
43
+ * Reclaims per run. Each freed slot lets another read stash and page again, so unbounded
44
+ * reclaim keeps a program spending storage queries that a full stash would have ended.
45
+ */
46
+ const RECLAIM_MAX_PER_RUN = 3;
34
47
  /**
35
48
  * The outer ceiling on one execution. It sits above both executors' own
36
49
  * deadlines (QuickJS 30s, the Dynamic Worker 60s), so a healthy executor
@@ -38,6 +51,41 @@ const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
38
51
  * never settles at all.
39
52
  */
40
53
  const EXECUTE_WATCHDOG_MS = CONFIG_DEFAULTS.execute.watchdogMs;
54
+ /**
55
+ * How long before a deadline a program's reads stop: the time the guest gets
56
+ * to receive their `timeout` and return what it has.
57
+ */
58
+ const READ_DEADLINE_MARGIN_MS = 1_000;
59
+ const readMarginMs = (deadlineMs) => Math.min(READ_DEADLINE_MARGIN_MS, Math.floor(deadlineMs / 10));
60
+ /**
61
+ * When a run's reads stop: before the executor's deadline, measured from now,
62
+ * and before the response ceiling, which queueing may already have spent.
63
+ */
64
+ function readDeadlineAt(executor, ceiling) {
65
+ const beforeCeiling = ceiling.at - readMarginMs(ceiling.ms);
66
+ const { deadlineMs } = executor;
67
+ if (deadlineMs === undefined)
68
+ return beforeCeiling;
69
+ return Math.min(beforeCeiling, Date.now() + deadlineMs - readMarginMs(deadlineMs));
70
+ }
71
+ /** Past the ceiling, room to store what a run leaves behind. Default 57 s against a 60 s client. */
72
+ const RESPONSE_STORAGE_MARGIN_MS = 2_000;
73
+ function responseCeiling(startedAt, ms) {
74
+ const at = startedAt + ms;
75
+ return { ms, at, startedAt, answerBy: at + Math.min(RESPONSE_STORAGE_MARGIN_MS, Math.floor(ms / 10)) };
76
+ }
77
+ /** Refuse a run with no time left to start. Nothing has run, so running it again is safe. */
78
+ const startable = (ceiling) => Effect.suspend(() => {
79
+ const elapsedMs = Date.now() - ceiling.startedAt;
80
+ return elapsedMs < ceiling.ms
81
+ ? Effect.void
82
+ : Effect.fail(new InvocationFailure({
83
+ code: "timeout",
84
+ message: `Operation "execute_code" used its ${ceiling.ms}ms response ceiling before it started, so it did not run and sent nothing. Run it again.`,
85
+ retryable: true,
86
+ details: { operation: "execute_code", stage: "response ceiling", elapsedMs, deadlineMs: ceiling.ms },
87
+ }));
88
+ });
41
89
  /**
42
90
  * Room to name every connector of a typical deployment. Past it, complete
43
91
  * entries plus an exact omission count stay inside this byte cap.
@@ -342,35 +390,22 @@ function serializedDiagnosticBytes(value) {
342
390
  }
343
391
  }
344
392
  function guestFailure(code, message, retryable = false) {
345
- return new InvocationFailure({ code, message, retryable });
393
+ return new InvocationFailure(["unavailable", "unsupported_capability", "input_required_unsupported"].includes(code)
394
+ ? utilityRecovery(code, message, retryable)
395
+ : { code, message, retryable });
346
396
  }
347
397
  /**
348
398
  * E1/X11: bound typed host failure metadata, including JSON escapes, before transport.
399
+ * The guest keeps this text, and may return it after a later call registers a credential it
400
+ * holds, so the message is cut between tokens.
349
401
  * Keep optional recovery whole: clipping an address or echoed args could turn
350
402
  * recovery into a different call. Oversized metadata is omitted instead.
351
403
  */
352
404
  function boundedGuestFailure(failure) {
353
- const boundText = (text, maxChars) => {
354
- if (JSON.stringify(text).length <= maxChars)
355
- return text;
356
- let low = 0;
357
- let high = Math.min(text.length, maxChars);
358
- while (low < high) {
359
- const mid = Math.ceil((low + high) / 2);
360
- if (JSON.stringify(`${text.slice(0, mid)}…`).length <= maxChars)
361
- low = mid;
362
- else
363
- high = mid - 1;
364
- }
365
- // Do not manufacture a lone surrogate when clipping a Unicode message.
366
- if (low > 0 && /[\uD800-\uDBFF]/.test(text[low - 1]))
367
- low--;
368
- return `${text.slice(0, low)}…`;
369
- };
370
405
  let details = {
371
406
  ...failure.details,
372
- code: boundText(failure.details.code, 128),
373
- message: boundText(failure.details.message, 2_000),
407
+ code: clipSerialized(failure.details.code, 128),
408
+ message: clipSerialized(failure.details.message, 2_000),
374
409
  };
375
410
  // Keep typed metadata below the executor bridge bound.
376
411
  if (JSON.stringify(details).length > 3_700) {
@@ -396,7 +431,7 @@ class HostCallBudgetExceeded extends InvocationFailure {
396
431
  constructor(hostCalls, maxHostCalls) {
397
432
  super({
398
433
  code: "budget_exceeded",
399
- message: `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`,
434
+ message: `execute_code host-call budget exceeded (${maxHostCalls} calls maximum); the run returned nothing. Split the rest across programs of at most ${maxHostCalls} calls each; do not resend any write it sent.`,
400
435
  retryable: false,
401
436
  });
402
437
  this.hostCalls = hostCalls;
@@ -426,6 +461,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
426
461
  readOnly: limits.readOnly,
427
462
  // A program that just missed an address cannot call search_tools.
428
463
  searchRoute: "connecta.search",
464
+ markUncallableWrites: limits.readProgram,
429
465
  concurrency: limits.discoveryConcurrency,
430
466
  probeTimeoutMs: limits.probeTimeoutMs,
431
467
  defer: limits.defer,
@@ -435,6 +471,8 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
435
471
  const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
436
472
  const hostCallTimeoutMs = Math.max(1, Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS));
437
473
  const { signal, diagnostics, programWrites } = limits;
474
+ /** The part of `requestedMs` left before reads stop; never below one tick. */
475
+ const readBudget = (requestedMs) => Math.min(requestedMs, Math.max(1, (limits.readDeadlineAt ?? Infinity) - Date.now()));
438
476
  const hostCalls = limits.hostCalls ?? { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
439
477
  let budgetFailure;
440
478
  let emitAttempts = 0;
@@ -455,33 +493,70 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
455
493
  const maxWrites = resolveBudget(limits.maxWrites, DEFAULT_MAX_WRITES);
456
494
  let writes = 0;
457
495
  // These are transfer cursors only. All result bytes live in the existing stash.
496
+ // `reclaim` marks a read's stash only this transfer can name, because the guest never saw
497
+ // its handle. A guest-supplied id stays readable until it expires; a write's stash expires too.
498
+ const readBytes = limits.readBytes ?? PROGRAM_RESULT_READ_BYTES;
458
499
  const transfers = new Map();
459
500
  const nativeResults = new Map();
460
- const transportResult = (value) => {
501
+ const transportResult = (value, write) => {
461
502
  if (value.format === "paged") {
462
503
  const handle = value;
463
504
  if (handle.resultId && handle.totalBytes <= PROGRAM_RESULT_INLINE_BYTES) {
464
505
  const transfer = `transfer:${crypto.randomUUID()}`;
465
- transfers.set(transfer, { id: handle.resultId, offset: 0 });
466
- return { transfer, handle };
506
+ transfers.set(transfer, {
507
+ id: handle.resultId,
508
+ offset: 0,
509
+ ...(write ? {} : { reclaim: { totalBytes: handle.totalBytes } }),
510
+ });
511
+ return { transfer, handle, ...(write ? {} : { reclaim: true }) };
467
512
  }
468
513
  }
469
514
  return value;
470
515
  };
516
+ // Slow storage may cost a run its reclaim, never its answer: all of a run's reclaim shares
517
+ // this much time, and once it's spent the rest of the run's entries just wait to expire.
518
+ let reclaimLeftMs = RECLAIM_BUDGET_MS;
519
+ let reclaimsLeft = RECLAIM_MAX_PER_RUN;
520
+ const reclaim = (id, totalBytes) => Effect.suspend(() => {
521
+ if (reclaimLeftMs <= 0 || reclaimsLeft <= 0)
522
+ return Effect.void;
523
+ reclaimsLeft--;
524
+ const started = Date.now();
525
+ return releaseStashedResult(registry, limits.heldResults, id, totalBytes).pipe(Effect.timeoutOrElse({ duration: Duration.millis(reclaimLeftMs), orElse: () => Effect.void }), Effect.ignore, Effect.ensuring(Effect.sync(() => {
526
+ reclaimLeftMs -= Date.now() - started;
527
+ })));
528
+ });
529
+ const dispatchDeadlineAt = limits.readDeadlineAt ?? limits.programDeadlineAt;
471
530
  /** Budget and account for writes a writable endpoint admits. */
472
- const invocationContext = (sending, timeoutMs) => ({
473
- source: "execute_code",
531
+ const invocationContext = (sending, timeoutMs, writeTimeoutMs) => ({
532
+ source: limits.readProgram ? READ_PROGRAM_SOURCE : "execute_code",
474
533
  sentSecrets,
475
534
  timeoutMs,
535
+ writeTimeoutMs,
476
536
  ...(limits.routes ? { routes: limits.routes } : {}),
477
537
  ...(signal !== undefined ? { requestSignal: signal } : {}),
478
538
  ...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
539
+ ...(dispatchDeadlineAt !== undefined
540
+ ? {
541
+ // Past the gate's time, a call's answer could not reach the program either.
542
+ dispatchDeadline: {
543
+ at: dispatchDeadlineAt,
544
+ refusal: (address) => ({
545
+ code: "timeout",
546
+ message: `Too little of this run's deadline remains for ${address}, so it was not sent. Return what you have and send it in a new program.`,
547
+ retryable: true,
548
+ details: { operation: address, stage: "dispatch gate" },
549
+ }),
550
+ },
551
+ }
552
+ : {}),
479
553
  unwrapResult: true,
480
554
  readOnly: limits.readOnly,
481
555
  processResult: authMethod((value, resolved, secrets, format, raw) => Effect.gen(function* () {
482
556
  // Dispatch succeeded before presentation began. Cancellation of paging
483
557
  // cannot turn this completed write into an unknown or failed write.
484
558
  sending.settle?.("ok");
559
+ const write = resolved.definition.classification !== "read";
485
560
  const page = (data, valueFormat, wireBytes) => authCall(pageProgramResult, [
486
561
  registry,
487
562
  baseUrl,
@@ -490,7 +565,11 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
490
565
  valueFormat,
491
566
  secrets,
492
567
  requestScope,
568
+ { max: maxHostCalls, left: maxHostCalls - hostCalls.attempted },
493
569
  wireBytes,
570
+ limits.followUpRoutes,
571
+ limits.heldResults,
572
+ readBytes,
494
573
  ]);
495
574
  const envelope = resolved.connector.kind === "mcp" && raw !== null && typeof raw === "object"
496
575
  ? raw
@@ -511,7 +590,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
511
590
  const content = envelope?.content;
512
591
  if (!Array.isArray(content) ||
513
592
  !content.some((block) => block && ["image", "audio", "resource", "resource_link"].includes(block.type))) {
514
- return transportResult(yield* page(value, format));
593
+ return transportResult(yield* page(value, format), write);
515
594
  }
516
595
  const unavailablePresentation = () => ({
517
596
  format: "paged",
@@ -537,7 +616,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
537
616
  catch (error) {
538
617
  if (!(error instanceof NativePresentationLimit))
539
618
  throw error;
540
- const result = dataKey === undefined ? undefined : transportResult(yield* page(value, format));
619
+ const result = dataKey === undefined ? undefined : transportResult(yield* page(value, format), write);
541
620
  const notice = unavailablePresentation();
542
621
  return result ? { value: result, native: { value: notice } } : notice;
543
622
  }
@@ -551,7 +630,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
551
630
  });
552
631
  let valueResult;
553
632
  if (native.format === "paged") {
554
- valueResult = result ? transportResult(result) : undefined;
633
+ valueResult = result ? transportResult(result, write) : undefined;
555
634
  if (!native.resultId || native.totalBytes > PROGRAM_RESULT_INLINE_BYTES) {
556
635
  return valueResult ? { value: valueResult, native: { value: native } } : native;
557
636
  }
@@ -561,14 +640,14 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
561
640
  // absent or connector-owned blocks.
562
641
  if (!retained) {
563
642
  const notice = native.format === "paged" ? native : unavailablePresentation();
564
- const valueResult = result ? transportResult(result) : undefined;
643
+ const valueResult = result ? transportResult(result, write) : undefined;
565
644
  return valueResult ? { value: valueResult, native: { value: notice } } : notice;
566
645
  }
567
646
  const ref = crypto.randomUUID();
568
647
  nativeResults.set(ref, retained);
569
648
  return {
570
649
  ...(valueResult ? { value: valueResult } : {}),
571
- native: { ref, value: transportResult(native), dataKey },
650
+ native: { ref, value: transportResult(native, write), dataKey },
572
651
  };
573
652
  })),
574
653
  beforeWrite: (target) => Effect.sync(() => {
@@ -583,13 +662,38 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
583
662
  unrecorded: true,
584
663
  };
585
664
  }
665
+ // A write is exactly what its program asked for, or it stays here. Reads
666
+ // keep the filtered value: sending one changes nothing it could not repeat.
667
+ if (sending.rewritten) {
668
+ return {
669
+ kind: "refuse",
670
+ error: {
671
+ code: "invalid_args",
672
+ message: `${target.connector.id}.${target.toolName} was not sent: its arguments hold a credential this request sent, and a write is never sent altered. If the value is genuine, send it as its own direct call.`,
673
+ retryable: false,
674
+ },
675
+ };
676
+ }
677
+ // Its answer could not reach the program, so refuse now while the outcome
678
+ // is known. Earlier writes go out and drain past the deadline (W9).
679
+ if (limits.readDeadlineAt !== undefined && Date.now() >= limits.readDeadlineAt) {
680
+ return {
681
+ kind: "refuse",
682
+ error: {
683
+ code: "timeout",
684
+ message: `Too little of this run's deadline remains for ${target.connector.id}.${target.toolName}, so it was not sent. Return what you have and send it in a new program.`,
685
+ retryable: true,
686
+ details: { operation: `${target.connector.id}.${target.toolName}`, stage: "write gate" },
687
+ },
688
+ };
689
+ }
586
690
  const budgeted = target.definition.classification !== "read";
587
691
  if (budgeted && writes >= maxWrites) {
588
692
  return {
589
693
  kind: "refuse",
590
694
  error: {
591
695
  code: "budget_exceeded",
592
- message: `execute_code write budget exceeded (${maxWrites} writes maximum, execute.maxWrites); ${target.connector.id}.${target.toolName} was not sent`,
696
+ message: `execute_code write budget exceeded (${maxWrites} writes per run); ${target.connector.id}.${target.toolName} was not sent. Check the run's writes count, then send only the rest in a new program; do not resend those that succeeded.`,
593
697
  retryable: false,
594
698
  },
595
699
  };
@@ -633,12 +737,14 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
633
737
  const requestedMs = normalizeTimeoutMs(timeout) ??
634
738
  normalizeTimeoutMs(limits.defaultToolTimeoutMs) ??
635
739
  hostCallTimeoutMs;
636
- const timeoutMs = Math.min(requestedMs, Math.max(1, (limits.programDeadlineAt ?? Infinity) - Date.now()));
740
+ const timeoutMs = readBudget(requestedMs);
741
+ const writeTimeoutMs = Math.min(requestedMs, Math.max(1, (limits.programDeadlineAt ?? Infinity) - Date.now()));
637
742
  // A program write settles here — unknown if the call never returned an
638
743
  // outcome. Credentials are filtered from arguments; submitted private values are not.
639
- const sending = {};
744
+ const filtered = sentSecrets.filterCredentials(args ?? {});
745
+ const sending = { rewritten: filtered.changed };
640
746
  const outcome = yield* invocation
641
- .pipeline(address, sentSecrets.redactCredentials(args ?? {}), invocationContext(sending, timeoutMs))
747
+ .pipeline(address, filtered.value, invocationContext(sending, timeoutMs, writeTimeoutMs))
642
748
  .pipe(Effect.onExit((exit) => Effect.sync(() => sending.settle?.(Exit.isSuccess(exit) ? writeStateOf(exit.value) : "unknown"))));
643
749
  if (outcome.resolved) {
644
750
  const resolved = outcome.resolved;
@@ -674,19 +780,38 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
674
780
  const discovery = (operation, read) => Effect.suspend(() => {
675
781
  const started = Date.now();
676
782
  const cancelled = () => guestFailure("cancelled", `connecta.${operation} was cancelled because the run ended.`);
677
- const reading = Effect.tryPromise({
678
- try: read,
679
- catch: (err) => (err instanceof DiscoveryPolicyError ? guestFailure(err.code, err.message) : err),
680
- });
681
- return (!hostAccessSignal
682
- ? reading
683
- : hostAccessSignal.aborted
684
- ? Effect.fail(cancelled())
685
- : Effect.raceAllFirst([reading, fromSignal(hostAccessSignal).pipe(Effect.mapError(cancelled))])).pipe(Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
783
+ // In the race itself, not behind a Promise: losing interrupts the catalog
784
+ // work and aborts its listing before the program hears the outcome (L3).
785
+ // The catalog hears the run's end as well, so its failure may arrive first.
786
+ const reading = Effect.suspend(read).pipe(Effect.catchDefect((err) => Effect.fail(err)), Effect.mapError((err) => hostAccessSignal?.aborted
787
+ ? cancelled()
788
+ : err instanceof DiscoveryPolicyError
789
+ ? guestFailure(err.code, err.message)
790
+ : err));
791
+ const contenders = [reading];
792
+ // A catalog probe may outlast the run; end it as a catchable timeout first.
793
+ if (limits.readDeadlineAt !== undefined)
794
+ contenders.push(Effect.suspend(() => {
795
+ const deadlineMs = readBudget(Infinity);
796
+ return Effect.sleep(Duration.millis(deadlineMs)).pipe(Effect.andThen(Effect.suspend(() => {
797
+ const elapsedMs = Date.now() - started;
798
+ const name = `connecta.${operation}`;
799
+ return Effect.fail(new InvocationFailure({
800
+ code: "timeout",
801
+ message: `Operation "${name}" timed out during catalog after ${elapsedMs}ms (effective deadline ${deadlineMs}ms).`,
802
+ retryable: true,
803
+ details: { operation: name, stage: "catalog", elapsedMs, deadlineMs },
804
+ }));
805
+ })));
806
+ }));
807
+ if (hostAccessSignal)
808
+ contenders.push(fromSignal(hostAccessSignal).pipe(Effect.mapError(cancelled)));
809
+ return (hostAccessSignal?.aborted ? Effect.fail(cancelled()) : Effect.raceAllFirst(contenders)).pipe(Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
686
810
  });
687
811
  const meta = createMetaTools(registry, baseUrl, {
688
812
  readOnly: limits.readOnly,
689
813
  requestScope,
814
+ heldResults: limits.heldResults,
690
815
  onResultRead: (dependencies) => limits.resultDependencies?.push(...dependencies),
691
816
  });
692
817
  const catalogDependency = (connector, tool) => {
@@ -704,63 +829,93 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
704
829
  catalogDependency(resolved.connector.id, resolved.toolName);
705
830
  };
706
831
  const operations = {
707
- // Uncounted: a failed program read a result field off an unawaited host call.
832
+ // Uncounted: a program read a result field off an unawaited host call.
708
833
  unawaited: () => {
709
834
  if (limits.unawaited)
710
835
  limits.unawaited.read = true;
711
836
  },
712
837
  call,
713
- result: (id, options, signal, onStage) => Effect.tryPromise({
714
- try: async () => {
715
- const transfer = typeof id === "string" ? transfers.get(id) : undefined;
716
- const transferId = id;
717
- if (transfer) {
718
- id = transfer.id;
719
- options = { offset: transfer.offset, maxBytes: PROGRAM_RESULT_PAGE_BYTES };
720
- }
721
- else if (id !== null && typeof id === "object" && !Array.isArray(id)) {
722
- id = id.resultId;
723
- }
724
- if (typeof id !== "string" ||
725
- (!options && options !== undefined) ||
726
- (options !== undefined && (typeof options !== "object" || Array.isArray(options)))) {
727
- throw guestFailure("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }).");
728
- }
729
- const args = { ...(options ?? {}) };
730
- if (Object.keys(args).some((key) => !["page", "offset", "maxBytes"].includes(key)) ||
731
- (args.page !== undefined &&
732
- (typeof args.page !== "number" ||
733
- !Number.isSafeInteger(args.page) ||
734
- args.page < 0 ||
735
- args.offset !== undefined)) ||
736
- (args.offset !== undefined &&
737
- (typeof args.offset !== "number" || !Number.isSafeInteger(args.offset) || args.offset < 0)) ||
738
- (args.maxBytes !== undefined &&
739
- (typeof args.maxBytes !== "number" || !Number.isSafeInteger(args.maxBytes) || args.maxBytes < 1))) {
740
- throw guestFailure("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }) with whole page/offset >= 0 and maxBytes >= 1; page and offset are mutually exclusive.");
741
- }
742
- // A page must fit every shipped bridge even when the connector cap is raised.
743
- args.maxBytes = Math.min(args.maxBytes ?? PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_PAGE_BYTES);
744
- const page = await meta.readResult({ id, ...args }, {
745
- signal: signal,
746
- onStage: onStage,
747
- });
748
- if (page.isError)
749
- throw new InvocationFailure((page.structuredContent?.error ?? {
750
- code: "not_found",
751
- message: page.content[0]?.text ?? "Result unavailable.",
752
- retryable: false,
753
- }));
754
- if (transfer) {
755
- if (page.structuredContent?.hasMore)
756
- transfer.offset = page.structuredContent.nextOffset;
757
- else
758
- transfers.delete(transferId);
759
- }
760
- return page.structuredContent;
761
- },
762
- catch: (err) => err,
763
- }),
838
+ result: (id, options, signal, onStage) => {
839
+ const finished = typeof id === "string" ? transfers.get(id) : undefined;
840
+ if (finished?.finished) {
841
+ // The prelude has the whole value, so a read gives its bytes back.
842
+ transfers.delete(id);
843
+ return finished.reclaim
844
+ ? reclaim(finished.id, finished.reclaim.totalBytes).pipe(Effect.as({}))
845
+ : Effect.succeed({});
846
+ }
847
+ return Effect.tryPromise({
848
+ try: async () => {
849
+ const transfer = typeof id === "string" ? transfers.get(id) : undefined;
850
+ const transferId = id;
851
+ if (transfer) {
852
+ id = transfer.id;
853
+ options = { offset: transfer.offset, maxBytes: PROGRAM_RESULT_PAGE_BYTES };
854
+ }
855
+ else if (id !== null && typeof id === "object" && !Array.isArray(id)) {
856
+ id = id.resultId;
857
+ }
858
+ if (typeof id !== "string" ||
859
+ (!options && options !== undefined) ||
860
+ (options !== undefined && (typeof options !== "object" || Array.isArray(options)))) {
861
+ throw new InvocationFailure(utilityRecovery("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? })."));
862
+ }
863
+ const args = { ...(options ?? {}) };
864
+ const whole = args.all === true;
865
+ if (args.all === false)
866
+ delete args.all;
867
+ if ((whole && Object.keys(args).length > 1) || (args.all !== undefined && !whole)) {
868
+ throw new InvocationFailure(utilityRecovery("invalid_args", "Use connecta.result(handleOrId, { all: true }) with no other options."));
869
+ }
870
+ delete args.all;
871
+ if (Object.keys(args).some((key) => !["page", "offset", "maxBytes"].includes(key)) ||
872
+ (args.page !== undefined &&
873
+ (typeof args.page !== "number" ||
874
+ !Number.isSafeInteger(args.page) ||
875
+ args.page < 0 ||
876
+ args.offset !== undefined)) ||
877
+ (args.offset !== undefined &&
878
+ (typeof args.offset !== "number" || !Number.isSafeInteger(args.offset) || args.offset < 0)) ||
879
+ (args.maxBytes !== undefined &&
880
+ (typeof args.maxBytes !== "number" || !Number.isSafeInteger(args.maxBytes) || args.maxBytes < 1))) {
881
+ throw new InvocationFailure(utilityRecovery("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }) with whole page/offset >= 0 and maxBytes >= 1; page and offset are mutually exclusive."));
882
+ }
883
+ // A page must fit every shipped bridge even when the connector cap is raised.
884
+ args.maxBytes = Math.min(args.maxBytes ?? PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_PAGE_BYTES);
885
+ const page = await meta.readResult({ id, ...args }, {
886
+ signal: signal,
887
+ onStage: onStage,
888
+ });
889
+ if (page.isError)
890
+ throw new InvocationFailure((page.structuredContent?.error ?? {
891
+ code: "not_found",
892
+ message: page.content[0]?.text ?? "Result unavailable.",
893
+ retryable: false,
894
+ }));
895
+ if (transfer) {
896
+ if (page.structuredContent?.hasMore)
897
+ transfer.offset = page.structuredContent.nextOffset;
898
+ else if (transfer.reclaim)
899
+ transfer.finished = true;
900
+ else
901
+ transfers.delete(transferId);
902
+ }
903
+ if (whole) {
904
+ const totalBytes = page.structuredContent?.totalBytes;
905
+ if (totalBytes > readBytes) {
906
+ throw guestFailure("result_too_large", `This result is ${totalBytes} bytes and connecta.result(id, { all: true }) reads at most ${readBytes}. Page it with { offset } and keep only what you need from each page.`);
907
+ }
908
+ if (!page.structuredContent?.hasMore)
909
+ return page.structuredContent;
910
+ const started = `transfer:${crypto.randomUUID()}`;
911
+ transfers.set(started, { id, offset: page.structuredContent.nextOffset });
912
+ return { ...page.structuredContent, transfer: started };
913
+ }
914
+ return page.structuredContent;
915
+ },
916
+ catch: (err) => err,
917
+ });
918
+ },
764
919
  skill: (name, _options, utilitySignal) => Effect.tryPromise({
765
920
  try: async () => {
766
921
  // Accept the top-level skills tool's { name } shape too: models copy it into programs.
@@ -768,6 +923,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
768
923
  if (typeof skillName !== "string")
769
924
  throw guestFailure("invalid_args", 'Use connecta.skill("usage") or connecta.skill("connector:<id>") with an exact skill name; the skills tool lists names.');
770
925
  const skills = new SkillsRegistry(registry, baseUrl, {
926
+ readOnly: limits.readOnly,
771
927
  requestScope,
772
928
  requestSignal: utilitySignal ?? hostAccessSignal,
773
929
  onRead: (connector, source) => limits.resultDependencies?.push({
@@ -780,7 +936,11 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
780
936
  });
781
937
  return { name: skillName, format: "text", text: await skills.text(skillName) };
782
938
  },
783
- catch: (err) => err instanceof ConnectorCallError ? guestFailure(err.code, err.message, err.code === "unavailable") : err,
939
+ catch: (err) => err instanceof SkillLookupError
940
+ ? new InvocationFailure(skillLookupRecovery(err.message))
941
+ : err instanceof ConnectorCallError
942
+ ? guestFailure(err.code, "Skills are unavailable.", err.retryable)
943
+ : err,
784
944
  }),
785
945
  read: (uri) => Effect.suspend(() => {
786
946
  const started = Date.now();
@@ -789,7 +949,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
789
949
  baseUrl,
790
950
  requestScope,
791
951
  sentSecrets,
792
- timeoutMs: hostCallTimeoutMs,
952
+ timeoutMs: readBudget(hostCallTimeoutMs),
793
953
  onResolved: (connector) => limits.resultDependencies?.push({ connector: connector.id, classification: "read" }),
794
954
  onConnectorTime: (elapsed) => {
795
955
  connectorMs += elapsed;
@@ -829,34 +989,40 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
829
989
  },
830
990
  catch: (err) => err,
831
991
  }),
832
- search: (raw) => discovery("search", async () => {
992
+ search: (raw) => discovery("search", () => {
833
993
  const args = (raw ?? {});
834
- const result = flatSearchResult(await catalog.search({
994
+ return catalog
995
+ .searchEffect({
835
996
  ...args,
836
997
  includeSchemas: args.includeSchemas ?? "typescript",
837
998
  includeSchemaKeys: args.includeSchemaKeys !== false,
838
- }, catalogDependency));
839
- boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
840
- return result;
999
+ }, catalogDependency)
1000
+ .pipe(Effect.map((page) => {
1001
+ const result = flatSearchResult(page);
1002
+ boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
1003
+ return result;
1004
+ }));
841
1005
  }),
842
- describe: (raw) => discovery("describe", async () => {
1006
+ describe: (raw) => discovery("describe", () => {
843
1007
  const args = (raw ?? {});
844
- const result = { tools: await catalog.describe({ ...args, format: args.format ?? "json" }) };
845
- boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
846
- for (const tool of result.tools) {
847
- if (!tool.error)
848
- catalogAddressDependency(tool.address);
849
- else {
850
- const resolved = registry.resolveAddress(tool.address);
851
- if (resolved)
852
- catalogDependency(resolved.connector.id);
853
- for (const connector of tool.errorDetails?.configuredConnectors ?? [])
854
- catalogDependency(connector);
855
- for (const suggestion of tool.errorDetails?.suggestions ?? [])
856
- catalogAddressDependency(suggestion);
1008
+ return catalog.describeEffect({ ...args, format: args.format ?? "json" }).pipe(Effect.map((tools) => {
1009
+ const result = { tools };
1010
+ boundedDiscoveryText(result, 'Split the address list, omit fullDescriptions, or use format: "compact".');
1011
+ for (const tool of result.tools) {
1012
+ if (!tool.error)
1013
+ catalogAddressDependency(tool.address);
1014
+ else {
1015
+ const resolved = registry.resolveAddress(tool.address);
1016
+ if (resolved)
1017
+ catalogDependency(resolved.connector.id);
1018
+ for (const connector of tool.errorDetails?.configuredConnectors ?? [])
1019
+ catalogDependency(connector);
1020
+ for (const suggestion of tool.errorDetails?.suggestions ?? [])
1021
+ catalogAddressDependency(suggestion);
1022
+ }
857
1023
  }
858
- }
859
- return result;
1024
+ return result;
1025
+ }));
860
1026
  }),
861
1027
  };
862
1028
  // Recoverable failures remain typed at the host-owned executor bridge. Terminal host-budget exhaustion bypasses it.
@@ -888,7 +1054,8 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
888
1054
  return [
889
1055
  name,
890
1056
  (...args) => {
891
- if (budgetFailure)
1057
+ // Once sealing begins the run's secret set is final, so nothing new may start.
1058
+ if (budgetFailure || limits.heldResults?.accepts() === false)
892
1059
  return stopped;
893
1060
  // Uncounted: one per run; any further call through the bridge ends the run.
894
1061
  if (budget === "once") {
@@ -912,23 +1079,27 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
912
1079
  if (counted)
913
1080
  hostCalls.admitted++;
914
1081
  const started = Date.now();
1082
+ const utilityDeadlineMs = readBudget(hostCallTimeoutMs);
915
1083
  let stage = deadline ?? "";
916
1084
  const invoke = Effect.suspend(() => operation(...args));
917
1085
  const guarded = deadline
918
1086
  ? withDeadlineEffect((utilitySignal) => Effect.suspend(() => operation(args[0], args[1], utilitySignal, (current) => {
919
1087
  stage = current;
920
1088
  })), {
921
- timeoutMs: hostCallTimeoutMs,
1089
+ timeoutMs: utilityDeadlineMs,
922
1090
  ...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
923
1091
  timeoutError: guestFailure("timeout", `connecta.${name} timed out.`),
924
1092
  })
925
1093
  : invoke;
926
- const settled = runEdge(guarded.pipe(Effect.catch((error) => {
1094
+ const operationEffect = activity?.recordOperation && ["search", "describe", "result", "skill"].includes(name)
1095
+ ? operationActivity(activity, registry, name, guarded, undefined, limits.readProgram ? READ_PROGRAM_SOURCE : undefined)
1096
+ : guarded;
1097
+ const running = runEdge(operationEffect.pipe(Effect.catch((error) => {
927
1098
  if (deadline && hostAccessSignal?.aborted)
928
1099
  return typedFailure(guestFailure("cancelled", `connecta.${name} was cancelled because the run ended.`));
929
1100
  if (deadline && error instanceof InvocationFailure && error.code === "timeout") {
930
1101
  const elapsedMs = Date.now() - started;
931
- const message = `Operation "connecta.${name}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${hostCallTimeoutMs}ms).`;
1102
+ const message = `Operation "connecta.${name}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${utilityDeadlineMs}ms).`;
932
1103
  return typedFailure(new InvocationFailure({
933
1104
  code: "timeout",
934
1105
  message,
@@ -937,12 +1108,14 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
937
1108
  operation: `connecta.${name}`,
938
1109
  stage,
939
1110
  elapsedMs,
940
- deadlineMs: hostCallTimeoutMs,
1111
+ deadlineMs: utilityDeadlineMs,
941
1112
  },
942
1113
  }));
943
1114
  }
944
1115
  return typedFailure(error);
945
- }))).then((value) => {
1116
+ })));
1117
+ limits.heldResults?.track(running);
1118
+ const settled = running.then((value) => {
946
1119
  if (counted)
947
1120
  hostCalls.succeeded++;
948
1121
  return budgetFailure ? stopped : redactAgentOutput(sentSecrets, value);
@@ -989,7 +1162,6 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
989
1162
  * waiting for one.
990
1163
  */
991
1164
  function awaitExecutor(start, signal, options = {}) {
992
- const started = Date.now();
993
1165
  const contenders = [
994
1166
  typeof start !== "function"
995
1167
  ? start
@@ -1016,21 +1188,34 @@ function awaitExecutor(start, signal, options = {}) {
1016
1188
  contenders.push(options.terminal);
1017
1189
  const { watchdog } = options;
1018
1190
  if (watchdog) {
1019
- contenders.push(Effect.sleep(Duration.millis(watchdog.ms)).pipe(Effect.andThen(Effect.suspend(() => {
1191
+ contenders.push(untilCeiling(watchdog.ceiling).pipe(Effect.andThen(Effect.suspend(() => {
1192
+ const { ms: deadlineMs, startedAt } = watchdog.ceiling;
1193
+ const elapsedMs = Date.now() - startedAt;
1194
+ // Time ran out before the executor's own deadline could: queueing
1195
+ // spent it, or that deadline is set past the ceiling. Nothing is wedged.
1196
+ if (watchdog.executorDeadlineAt !== undefined && Date.now() < watchdog.executorDeadlineAt) {
1197
+ return Effect.fail(new InvocationFailure({
1198
+ code: "timeout",
1199
+ message: `Operation "execute_code" timed out during response ceiling after ${elapsedMs}ms (effective deadline ${deadlineMs}ms). Return sooner, or do less in one program.`,
1200
+ retryable: true,
1201
+ details: { operation: "execute_code", stage: "response ceiling", elapsedMs, deadlineMs },
1202
+ }));
1203
+ }
1020
1204
  watchdog.logger.warn("[connecta] execute_code executor did not settle; run abandoned", {
1021
- watchdogMs: watchdog.ms,
1205
+ watchdogMs: deadlineMs,
1022
1206
  });
1023
- const elapsedMs = Date.now() - started;
1024
1207
  return Effect.fail(new InvocationFailure({
1025
1208
  code: "timeout",
1026
- message: `Operation "execute_code" timed out during sandbox watchdog after ${elapsedMs}ms (effective deadline ${watchdog.ms}ms); sandbox unresponsive.`,
1209
+ message: `Operation "execute_code" timed out during sandbox watchdog after ${elapsedMs}ms (effective deadline ${deadlineMs}ms); sandbox unresponsive.`,
1027
1210
  retryable: false,
1028
- details: { operation: "execute_code", stage: "sandbox watchdog", elapsedMs, deadlineMs: watchdog.ms },
1211
+ details: { operation: "execute_code", stage: "sandbox watchdog", elapsedMs, deadlineMs },
1029
1212
  }));
1030
1213
  }))));
1031
1214
  }
1032
1215
  return Effect.raceAllFirst(contenders);
1033
1216
  }
1217
+ /** Waits until the ceiling; already past it, one timer turn. */
1218
+ const untilCeiling = (ceiling) => Effect.suspend(() => Effect.sleep(Duration.millis(Math.max(1, ceiling.at - Date.now()))));
1034
1219
  /**
1035
1220
  * The run's own signal, aborted however the run ends. It follows the
1036
1221
  * caller's, and aborting it on the way out is what releases outstanding host
@@ -1059,9 +1244,11 @@ function runSignal(caller) {
1059
1244
  * executor, so this request's providers and program run here, never in the
1060
1245
  * releasing request, which workerd refuses.
1061
1246
  */
1062
- function leased(executor, signal) {
1247
+ function leased(executor, signal, ceiling) {
1063
1248
  return Effect.acquireRelease(awaitExecutor(() => executor.acquire({ signal, wait: true }), signal, {
1064
1249
  late: (lease) => lease.release(),
1250
+ // A queue configured to outwait the client still answers it.
1251
+ terminal: untilCeiling(ceiling).pipe(Effect.andThen(Effect.suspend(() => Effect.fail(new ExecutorAdmissionError("executor_overloaded", `Executor admission timed out after ${Date.now() - ceiling.startedAt}ms.`))))),
1065
1252
  }), (lease) => Effect.sync(() => lease.release()));
1066
1253
  }
1067
1254
  /** The execute_code handler. Exported for direct testing. */
@@ -1072,12 +1259,13 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1072
1259
  };
1073
1260
  const hostCallTimeoutMs = resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS);
1074
1261
  const maxResultChars = resolveBudget(config.maxResultChars, MAX_EXECUTE_RESULT_CHARS);
1075
- const play = (program, diagnosticsRequested, callerSignal, requestScope, hostCalls, programWrites) => Effect.suspend(() => {
1262
+ const readBytes = resolveBudget(executor.maxReadBytes, PROGRAM_RESULT_READ_BYTES);
1263
+ const play = (program, diagnosticsRequested, callerSignal, requestScope, hostCalls, programWrites, unawaited, retention, ceiling) => Effect.suspend(() => {
1076
1264
  const sentSecrets = sentSecretsForRequest(requestScope);
1265
+ const heldResults = new HeldResults(sentSecrets, requestOwner(requestScope).credentialReads);
1077
1266
  const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
1078
1267
  const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics, maxResultChars);
1079
1268
  const resultDependencies = [];
1080
- const unawaited = { read: false };
1081
1269
  const dispatchController = new AbortController();
1082
1270
  const terminal = Deferred.makeUnsafe();
1083
1271
  let budgetFailure;
@@ -1092,6 +1280,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1092
1280
  // every exit, using the existing bounded cleanup and deferred tail.
1093
1281
  yield* Effect.addFinalizer(() => Effect.forEach(registry.listConnectors(), (connector) => closeScope(connector, registry.contextFor(connector.id, baseUrl, requestScope), config.defer), { concurrency: "unbounded", discard: true }));
1094
1282
  const signal = yield* runSignal(callerSignal);
1283
+ yield* startable(ceiling);
1095
1284
  // Admission comes before provider construction: queued calls retain
1096
1285
  // no catalogs or provider closures.
1097
1286
  let lease;
@@ -1099,7 +1288,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1099
1288
  lease = yield* timed((elapsed) => {
1100
1289
  if (diagnostics)
1101
1290
  diagnostics.admissionMs = elapsed;
1102
- }, leased(executor, signal));
1291
+ }, leased(executor, signal, ceiling));
1103
1292
  if ((lease.waitMs ?? 0) > 0) {
1104
1293
  logger.debug("[connecta] execute_code admitted after queue wait", {
1105
1294
  waitMs: lease.waitMs,
@@ -1124,19 +1313,25 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1124
1313
  maxHostCalls: config.maxHostCalls,
1125
1314
  hostCallTimeoutMs,
1126
1315
  defaultToolTimeoutMs: config.defaultToolTimeoutMs,
1127
- programDeadlineAt: Date.now() + watchdog.ms,
1316
+ programDeadlineAt: ceiling.at,
1317
+ readDeadlineAt: readDeadlineAt(executor, ceiling),
1128
1318
  defer: config.defer,
1129
1319
  readOnly: config.readOnly,
1130
1320
  maxWrites: config.maxWrites,
1131
1321
  routes: config.routes,
1322
+ readProgram: config.readProgram,
1323
+ followUpRoutes: config.followUpRoutes,
1132
1324
  programWrites,
1325
+ readBytes,
1133
1326
  resultDependencies,
1134
1327
  dispatchController,
1135
1328
  unawaited,
1329
+ heldResults,
1136
1330
  }, requestScope)));
1137
1331
  if (signal.aborted) {
1138
1332
  return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
1139
1333
  }
1334
+ yield* startable(ceiling);
1140
1335
  const admitted = lease;
1141
1336
  // A program write may still be on the wire when the program settles:
1142
1337
  // close, so a write gated after this is not sent, then let the ones
@@ -1156,7 +1351,19 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1156
1351
  interrupted: (logs) => {
1157
1352
  executorLogs = sentSecrets.redact(logs);
1158
1353
  },
1159
- }).pipe(Effect.map((outcome) => sentSecrets.redact(outcome)), Effect.mapError((error) => sentSecrets.redact(error))), signal, { watchdog, terminal: Deferred.await(terminal) }).pipe(Effect.ensuring(Effect.suspend(() => {
1354
+ }).pipe(Effect.map((outcome) => sentSecrets.redact(outcome)), Effect.mapError((error) => sentSecrets.redact(error))), signal, {
1355
+ watchdog: {
1356
+ ceiling,
1357
+ // With no published deadline, a run that queued has not shown it is wedged.
1358
+ executorDeadlineAt: executor.deadlineMs !== undefined
1359
+ ? Date.now() + executor.deadlineMs
1360
+ : (admitted?.waitMs ?? 0) > 0
1361
+ ? Infinity
1362
+ : undefined,
1363
+ logger: watchdog.logger,
1364
+ },
1365
+ terminal: Deferred.await(terminal),
1366
+ }).pipe(Effect.ensuring(Effect.suspend(() => {
1160
1367
  programWrites.close();
1161
1368
  dispatchController.abort();
1162
1369
  // Finalizers are masked by default. The request must be able
@@ -1180,6 +1387,10 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1180
1387
  maxResultChars - emitted.textChars,
1181
1388
  programWrites.hasSucceeded,
1182
1389
  maxResultChars,
1390
+ heldResults,
1391
+ ceiling.answerBy,
1392
+ readBytes,
1393
+ config.followUpRoutes,
1183
1394
  ]),
1184
1395
  };
1185
1396
  const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
@@ -1187,7 +1398,15 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1187
1398
  // the retained log prefix. Give that report one timer turn, just as
1188
1399
  // cancellation does in awaitExecutor; never wait on a Worker isolate.
1189
1400
  budgetFailure ? Effect.sleep(Duration.millis(1)).pipe(Effect.as(exit)) : Effect.succeed(exit)));
1190
- return Effect.flatMap(exited, (exit) => {
1401
+ // Every way a run answers can carry a result id, so every one of them seals, by the
1402
+ // time the response must be on its way. A sealing cut short leaves entries unreadable.
1403
+ const sealRun = Effect.suspend(() => sealHeldResults(heldResults, registry.resultsStorage(), ceiling.answerBy, callerSignal)).pipe(Effect.tap(({ refused, refusedWrite }) => Effect.sync(() => {
1404
+ if (refused === 0)
1405
+ return;
1406
+ retention.note = refusedWrite ? RESULTS_REFUSED_WRITE_NOTE : RESULTS_REFUSED_NOTE;
1407
+ logFailure(logger, "result paging unavailable", failureRecord({}));
1408
+ })));
1409
+ const answered = Effect.flatMap(exited, (exit) => {
1191
1410
  // A synchronous fire-and-forget burst can also settle its executor in
1192
1411
  // this turn. The host's terminal refusal always wins over that value.
1193
1412
  if (budgetFailure instanceof HostCallBudgetExceeded) {
@@ -1211,6 +1430,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1211
1430
  timeoutError: cancelled,
1212
1431
  }).pipe(Effect.catch((error) => Effect.succeed(failedRun(callerSignal?.aborted ? cancelled : error, logger, reported))));
1213
1432
  });
1433
+ return Effect.tap(answered, () => sealRun);
1214
1434
  });
1215
1435
  return authMethod(({ code, diagnostics }, options = {}) => Effect.suspend(() => {
1216
1436
  const requestScope = config.requestScope ?? {};
@@ -1220,6 +1440,9 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1220
1440
  return Effect.fail(owner.controller.signal.reason);
1221
1441
  const hostCalls = { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
1222
1442
  const programWrites = new ProgramWrites();
1443
+ const unawaited = { read: false };
1444
+ const retention = {};
1445
+ const ceiling = responseCeiling(config.requestStartedAt ?? Date.now(), watchdog.ms);
1223
1446
  const response = Effect.suspend(() => {
1224
1447
  // A code-unit count above the cap is already too large in UTF-8. Check
1225
1448
  // that first so a huge direct-call string is never encoded in full.
@@ -1228,22 +1451,53 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1228
1451
  const message = `execute_code code exceeds the ${EXECUTE_MAX_CODE_BYTES}-byte UTF-8 limit.`;
1229
1452
  return Effect.succeed(failureResponse(message, { code: { code: "invalid_args", message, retryable: false } }));
1230
1453
  }
1231
- return play(normalizeProgramSource(code), diagnostics, options.signal, requestScope, hostCalls, programWrites);
1454
+ return play(normalizeProgramSource(code), diagnostics, options.signal, requestScope, hostCalls, programWrites, unawaited, retention, ceiling);
1232
1455
  });
1233
- return response.pipe(Effect.provideService(RequestScope, owner), Effect.map((unfinished) => {
1456
+ return operationActivity(activity, registry, "program", response.pipe(Effect.provideService(RequestScope, owner), Effect.map((unfinished) => {
1234
1457
  // Every response, including intake refusals, crosses accounting and
1235
1458
  // exactly one envelope check at this operation boundary.
1236
- const result = redactAgentOutput(sentSecrets, programWrites.finish(unfinished));
1459
+ const result = redactAgentOutput(sentSecrets, programWrites.finish(unfinished, sentSecrets, maxResultChars));
1237
1460
  // Calls abandoned by the guest are failed when the run cancels them.
1238
1461
  // Connecta attaches its own integers after the envelope check.
1239
1462
  const counts = { ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded };
1240
- result.structuredContent = { ...result.structuredContent, hostCalls: counts };
1463
+ const note = [
1464
+ finishedRunNote(result, hostCalls.attempted, programWrites.sent, unawaited.read),
1465
+ retention.note,
1466
+ ]
1467
+ .filter(Boolean)
1468
+ .join(" ");
1469
+ result.structuredContent = {
1470
+ ...result.structuredContent,
1471
+ ...(note ? { note } : {}),
1472
+ hostCalls: counts,
1473
+ };
1241
1474
  if (result.content[0]?.type === "text")
1242
1475
  result.content[0].text = JSON.stringify(result.structuredContent);
1243
1476
  return result;
1244
- }), Effect.catchDefect(Effect.fail), Effect.mapError((error) => redactAgentOutput(sentSecrets, error)));
1477
+ }), Effect.catchDefect(Effect.fail), Effect.mapError((error) => redactAgentOutput(sentSecrets, error))), () => ({ ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded }), config.readProgram ? READ_PROGRAM_SOURCE : undefined);
1245
1478
  }));
1246
1479
  }
1480
+ const RESULTS_REFUSED_NOTE = "A result this run retained was not kept for later runs, so paging its id from a new program may fail. Rerun the read for a fresh result.";
1481
+ const RESULTS_REFUSED_WRITE_NOTE = `${RESULTS_REFUSED_NOTE} A write's output among them may not be retrievable: that write already ran, so do not repeat it.`;
1482
+ const UNAWAITED_HINT = "connecta.* returns a Promise; await it: (await connecta.call(address, args)).data.";
1483
+ const SENT_NOTE = "Its writes were already sent. Do not rerun it, even with a fix: read their targets in a new program.";
1484
+ /**
1485
+ * A completed run that gave the agent nothing to read looks like nothing
1486
+ * happened, and a write inside it is exactly what must not run again (INV-9).
1487
+ * Fixed text only: attached after the envelope check, like the counters.
1488
+ */
1489
+ function finishedRunNote(result, attempted, wrote, unawaited) {
1490
+ if (result.isError)
1491
+ return undefined;
1492
+ if (unawaited)
1493
+ return wrote ? `${UNAWAITED_HINT} ${SENT_NOTE}` : UNAWAITED_HINT;
1494
+ const body = result.structuredContent;
1495
+ if (body?.result !== undefined || body?.emitted !== undefined)
1496
+ return undefined;
1497
+ if (wrote)
1498
+ return `Program returned nothing. ${SENT_NOTE}`;
1499
+ return `Program returned nothing${attempted > 0 ? ", though its calls ran" : " and made no host calls"}. Add a return.`;
1500
+ }
1247
1501
  /** An execution that never produced an ExecuteResult, as the model sees it. */
1248
1502
  function failedRun(err, logger, { emitted, diagnostics, program }) {
1249
1503
  const logs = err !== null && typeof err === "object" && "logs" in err ? executeLogs(err.logs) : undefined;
@@ -1384,67 +1638,87 @@ function failureResponse(message, options) {
1384
1638
  function discardedEmits(emitted) {
1385
1639
  return emitted.blocks.length > 0 ? { emittedDiscarded: emitted.blocks.length } : {};
1386
1640
  }
1387
- const inventoryWords = (text) => text
1388
- .toLowerCase()
1389
- .split(/[^\p{L}\p{N}]+/u)
1390
- .filter(Boolean);
1391
- /** A title that only respells the id ("Google Calendar" for google_calendar) adds nothing. */
1392
- function titleAddsInformation(id, title) {
1393
- const words = inventoryWords(title);
1394
- const idWords = inventoryWords(id);
1395
- return words.join("") !== idWords.join("") && words.some((word) => !idWords.includes(word));
1396
- }
1641
+ /** Follows the inventory only when an entry shown carries the mark; `*` cannot appear in an id. */
1642
+ const REQUIRED_GUIDE_LEGEND = ' *Read connecta.skill("connector:<id>") before its first call.';
1643
+ /** Deployment configuration, normalized and capped like a title: a one-line purpose, never a paragraph. */
1644
+ const CONNECTOR_PURPOSE_MAX_BYTES = 57;
1397
1645
  /**
1398
1646
  * Every configured connector, from configuration alone. Detail degrades before
1399
- * names do: titles go first, then optional guide pointers, and only past the
1400
- * byte cap do whole entries give way to an exact omission count.
1647
+ * names do: purposes go first, from the end of registry order, then titles, and
1648
+ * only past the byte cap do whole entries give way to an exact omission count.
1649
+ * Search and describe name optional guides.
1401
1650
  */
1402
1651
  function connectorInventory(connectors) {
1403
1652
  const prefix = "Connectors: ";
1404
1653
  if (connectors.length === 0)
1405
1654
  return `${prefix}none.`;
1406
1655
  const fits = (line) => boundedEchoText(line, CONNECTOR_INVENTORY_MAX_BYTES) === line;
1407
- const entry = (connector, detail) => {
1408
- const notes = [];
1409
- const title = connector.title?.replace(/\s+/g, " ").trim();
1410
- if (detail >= 2 && title && titleAddsInformation(connector.id, title))
1411
- notes.push(boundedEchoText(title, 45));
1412
- const required = connectorGuideRequired(connector);
1413
- if (connectorGuide(connector) && (required || detail >= 1)) {
1414
- const fetch = `skills({name:"${connectorSkillName(connector.id)}"})`;
1415
- notes.push(required ? `read first: ${fetch}` : `guide: ${fetch}`);
1416
- }
1417
- return notes.length > 0 ? `${connector.id} (${notes.join("; ")})` : connector.id;
1656
+ const guideFirst = (connector) => connectorGuide(connector) !== undefined && connectorGuideRequired(connector);
1657
+ const legend = (shown) => (shown.some(guideFirst) ? REQUIRED_GUIDE_LEGEND : "");
1658
+ const entry = (connector, titled, withPurpose = false) => {
1659
+ const name = guideFirst(connector) ? `${connector.id}*` : connector.id;
1660
+ const title = titled ? connectorInventoryTitle(connector) : undefined;
1661
+ const description = connector.description?.replace(/\s+/g, " ").trim().replace(/\.$/, "");
1662
+ const shown = `${connector.id} ${connector.title?.replace(/\s+/g, " ").trim() ?? ""}`;
1663
+ const purpose = withPurpose && description && addsInformation(shown, description)
1664
+ ? inventoryNote(description, CONNECTOR_PURPOSE_MAX_BYTES)
1665
+ : undefined;
1666
+ const notes = [title, purpose].filter((note) => note !== undefined);
1667
+ return notes.length > 0 ? `${name} (${notes.join("; ")})` : name;
1418
1668
  };
1419
- for (const detail of [2, 1, 0]) {
1420
- const line = `${prefix}${connectors.map((connector) => entry(connector, detail)).join(", ")}.`;
1421
- if (fits(line))
1422
- return line;
1669
+ const described = (purposes) => `${prefix}${connectors.map((connector, index) => entry(connector, true, index < purposes)).join(", ")}.${legend(connectors)}`;
1670
+ if (fits(described(0))) {
1671
+ // The most entries, in registry order, whose purposes still fit.
1672
+ let shown = 0;
1673
+ let over = connectors.length + 1;
1674
+ while (over - shown > 1) {
1675
+ const middle = Math.floor((shown + over) / 2);
1676
+ if (fits(described(middle)))
1677
+ shown = middle;
1678
+ else
1679
+ over = middle;
1680
+ }
1681
+ return described(shown);
1423
1682
  }
1424
- const entries = connectors.map((connector) => entry(connector, 0));
1683
+ const untitled = `${prefix}${connectors.map((connector) => entry(connector, false)).join(", ")}.${legend(connectors)}`;
1684
+ if (fits(untitled))
1685
+ return untitled;
1686
+ const truncated = (shown) => {
1687
+ const kept = connectors.slice(0, shown);
1688
+ const names = kept.map((connector) => entry(connector, false)).join(", ");
1689
+ return `${prefix}${names}${shown > 0 ? "; " : ""}+${connectors.length - shown} more.${legend(kept)}`;
1690
+ };
1425
1691
  let shown = 0;
1426
- while (fits(`${prefix}${entries.slice(0, shown + 1).join(", ")}; +${entries.length - shown - 1} more.`))
1692
+ while (fits(truncated(shown + 1)))
1427
1693
  shown++;
1428
- return `${prefix}${entries.slice(0, shown).join(", ")}${shown > 0 ? "; " : ""}+${entries.length - shown} more.`;
1694
+ return truncated(shown);
1429
1695
  }
1430
- const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, readOnly, native = false, routes = META_ROUTES) => `${native ? `Call tools directly; combine work here using their program addresses. ${readOnly ? "Read-only endpoint: writes are omitted and refused." : "Writable endpoint: programs may read and write; the client approves."}` : `${routes === routeNames("code") ? `Every operation uses ${routes.program}.` : readOnly ? `One known read: ${routes.read}. Everything else: ${routes.program}.` : `One known read: ${routes.read}. One known write: ${routes.write}. Everything else: ${routes.program}.`} ${readOnly ? "Read-only endpoint: search and describe omit write tools, and every write call is refused." : "Writable endpoint: programs may read and write; the client approves."}`} Limits: ${hostLimits.maxHostCalls} host calls, ${readOnly ? "" : `${maxWrites} writes, `}${hostLimits.callTimeoutMs === hostLimits.hostCallTimeoutMs ? `${hostLimits.callTimeoutMs / 1_000}s/host call` : `${hostLimits.callTimeoutMs / 1_000}s/call (${hostLimits.hostCallTimeoutMs / 1_000}s result/skill/read)`}.
1696
+ /**
1697
+ * Short by design: Codex carries every description in each turn's tool list and
1698
+ * `execute_code` already holds the full declaration. It still names the guest
1699
+ * calls and the connectors, so a host that loads only this tool can write a
1700
+ * program and tell similar connectors apart.
1701
+ */
1702
+ const READ_PROGRAM_DESCRIPTION = (hostLimits, inventory) => `Run a read-only JavaScript program without write approval: same sandbox and connecta API as ${META_ROUTES.program}, ${hostLimits.maxHostCalls} host calls. Use it for read work at an unknown address, joins, loops and reductions. Each call that can write is refused before dispatch with its next step, and catching the refusal sends nothing. search and describe list those tools with callable: false; ${META_ROUTES.write} and ${META_ROUTES.program} run them. Use async () => { ... } with connecta.search({ query }), connecta.describe({ address }), connecta.call(address, args), connecta.result(resultId, { page }), and return the answer.
1703
+ ${inventory}`;
1704
+ const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, readOnly, native = false, routes = META_ROUTES, readRoute = false) => `${native ? `Call tools directly; combine work here using their program addresses. ${readOnly ? "Read-only endpoint: writes are omitted and refused." : "Writable endpoint: programs may write; the host approves."}` : `${routes === routeNames("code") ? `Every operation uses ${routes.program}.` : readOnly ? `One known read: ${routes.read}. Everything else: ${routes.program}.` : readRoute ? `One known read: ${routes.read}. One known write: ${routes.write}. Read programs: ${READ_PROGRAM_SOURCE}. Other: ${routes.program}.` : `One known read: ${routes.read}. One known write: ${routes.write}. Everything else: ${routes.program}.`} ${readOnly ? "Read-only endpoint: search and describe omit write tools, and every write call is refused." : "Writable endpoint: programs may write; the host approves."}`} Limits: ${hostLimits.maxHostCalls} host calls, ${readOnly ? "" : `${maxWrites} writes, `}${hostLimits.callTimeoutMs === hostLimits.hostCallTimeoutMs ? `${hostLimits.callTimeoutMs / 1_000}s/host call` : `${hostLimits.callTimeoutMs / 1_000}s/call (${hostLimits.hostCallTimeoutMs / 1_000}s result/skill/read)`}${hostLimits.runDeadlineMs === undefined ? "" : `, ${hostLimits.runDeadlineMs / 1_000}s/run`}.
1431
1705
  ${connectorInventory(connectors)}
1432
1706
 
1433
- Use async () => { ... }, no arguments. No portable ambient capabilities. API:
1707
+ Use async () => { ... }, no arguments. No imports, require, fetch, filesystem or timers. API:
1434
1708
  \`\`\`ts
1435
1709
  ${GUEST_API_DECLARATION}
1436
1710
  \`\`\`
1437
- search and describe return objects, not arrays. Search shows a TypeScript signature by default; describe, JSON Schema. A call result over 1 MiB returns a paged handle; read it with connecta.result(handle, { offset }), which pages by UTF-8 byte offset. Every function except ${UNCOUNTED_GUEST_FUNCTIONS} spends a host call. Promise.allSettled keeps per-call errors, but exceeding the host-call limit ends the program. When paging is available, an oversized program return carries resultId and nextAction.
1711
+ search and describe return objects, not arrays.${connectorGuides ? " A row with guideRequired: read its connector guide first." : ""} Search shows a TypeScript signature by default; describe, JSON Schema. A call result over 1 MiB returns a paged handle: follow its hint. Every function except ${UNCOUNTED_GUEST_FUNCTIONS} spends a host call; over the limit the program returns nothing, even under Promise.allSettled. Near the host-call limit, return page.nextCursor to resume next program. An oversized return carries nextAction.
1438
1712
 
1439
- Never repeat writes for output. emit(result) or emit(block) from result.content forwards MCP blocks unchanged; an authored Block's image/audio data is base64. emit needs no await. Caps: ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. Text shares result cap. connecta.skill("usage"): repair${connectorGuides ? ", guide handling" : ""}.`;
1713
+ ${readOnly ? "" : "Never repeat writes for output. "}emit(result) or emit(block) forwards MCP blocks; authored image/audio data is base64. Caps: ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes; text shares the return cap. connecta.skill("usage"): repair.`;
1440
1714
  // Module scope, like the other five meta-tool inputs: its JSON Schema is
1441
1715
  // derived once per process. Budgets and connectors vary by deployment and
1442
1716
  // view, so they live in the description, never here.
1443
1717
  const EXECUTE_INPUT = advertisedSchema(z.object({
1444
1718
  code: z
1445
1719
  .string()
1446
- .describe("One complete zero-argument JavaScript async arrow: async () => { ... }. Use the provided connecta global to discover, call, and return the reduced answer. At most 65,536 UTF-8 bytes."),
1447
- diagnostics: z.boolean().optional().describe("Add request-local, payload-free timing and result-size summaries."),
1720
+ .describe("Source text of one zero-argument JavaScript async arrow: async () => { ...; return answer; }. Pass a string, even from a JavaScript host. Use the connecta global. At most 65,536 UTF-8 bytes."),
1721
+ diagnostics: z.boolean().optional().describe("Add timing and result-size summaries."),
1448
1722
  }));
1449
1723
  /** Register the execute_code meta-tool. Only called when an executor is configured. */
1450
1724
  export function registerExecuteTool(server, registry, ctx) {
@@ -1462,7 +1736,7 @@ export function registerExecuteTool(server, registry, ctx) {
1462
1736
  };
1463
1737
  const connectors = registry.listConnectors();
1464
1738
  const maxWrites = resolveBudget(ctx.maxWrites, DEFAULT_MAX_WRITES);
1465
- const execute = createExecuteTool(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, {
1739
+ const runnerConfig = {
1466
1740
  defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
1467
1741
  discoveryConcurrency: ctx.discoveryConcurrency,
1468
1742
  probeTimeoutMs: ctx.probeTimeoutMs,
@@ -1476,23 +1750,11 @@ export function registerExecuteTool(server, registry, ctx) {
1476
1750
  maxWrites,
1477
1751
  maxResultChars: ctx.maxResultChars,
1478
1752
  requestScope: ctx.requestScope,
1753
+ requestStartedAt: ctx.requestStartedAt,
1479
1754
  routes: ctx.routes,
1480
- });
1481
- server.registerTool("execute_code", {
1482
- description: executeDescription(emitBudgets, {
1483
- ...hostLimits,
1484
- // connecta.call falls back to calls.defaultTimeoutMs; result, skill and read always use hostCallTimeoutMs.
1485
- callTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
1486
- }, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true, ctx.routes),
1487
- inputSchema: EXECUTE_INPUT,
1488
- outputSchema: EXECUTE_OUTPUT,
1489
- // The client sees execute_code as a write exactly when the endpoint writes.
1490
- annotations: {
1491
- readOnlyHint: ctx.readOnly === true,
1492
- destructiveHint: ctx.readOnly !== true,
1493
- openWorldHint: true,
1494
- },
1495
- }, authMethod((args, extra) => Effect.gen(function* () {
1755
+ ...(ctx.readRoute ? { followUpRoutes: routeNames("read-route") } : {}),
1756
+ };
1757
+ const programHandler = (execute, name) => (args, extra) => Effect.gen(function* () {
1496
1758
  if (ctx.client)
1497
1759
  bindMcpClient(extra, ctx.client, ctx.activity);
1498
1760
  const run = authMethod(() => Effect.suspend(() => {
@@ -1516,8 +1778,35 @@ export function registerExecuteTool(server, registry, ctx) {
1516
1778
  })));
1517
1779
  }));
1518
1780
  const result = yield* ctx.authElicitation
1519
- ? authInvoke(ctx.authElicitation, "run", ["execute_code", args, extra, run])
1781
+ ? authInvoke(ctx.authElicitation, "run", [name, args, extra, run])
1520
1782
  : authCall(run, []);
1521
1783
  return ctx.native ? nativeRecovery(result) : result;
1522
- })));
1784
+ });
1785
+ const newRunner = (config) => createExecuteTool(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, config);
1786
+ server.registerTool("execute_code", {
1787
+ description: executeDescription(emitBudgets, {
1788
+ ...hostLimits,
1789
+ // connecta.call falls back to calls.defaultTimeoutMs; result, skill and read always use hostCallTimeoutMs.
1790
+ callTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
1791
+ ...(ctx.executor.deadlineMs !== undefined
1792
+ ? { runDeadlineMs: Math.min(ctx.executor.deadlineMs, resolveBudget(ctx.watchdogMs, EXECUTE_WATCHDOG_MS)) }
1793
+ : {}),
1794
+ }, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true, ctx.routes, ctx.readRoute === true),
1795
+ inputSchema: EXECUTE_INPUT,
1796
+ outputSchema: EXECUTE_OUTPUT,
1797
+ // The client sees execute_code as a write exactly when the endpoint writes.
1798
+ annotations: {
1799
+ readOnlyHint: ctx.readOnly === true,
1800
+ destructiveHint: ctx.readOnly !== true,
1801
+ openWorldHint: true,
1802
+ },
1803
+ }, authMethod(programHandler(newRunner(runnerConfig), "execute_code")));
1804
+ if (ctx.readRoute) {
1805
+ server.registerTool(READ_PROGRAM_SOURCE, {
1806
+ description: READ_PROGRAM_DESCRIPTION(hostLimits, connectorInventory(connectors)),
1807
+ inputSchema: EXECUTE_INPUT,
1808
+ outputSchema: EXECUTE_OUTPUT,
1809
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
1810
+ }, authMethod(programHandler(newRunner({ ...runnerConfig, readProgram: true, routes: routeNames("read-route") }), READ_PROGRAM_SOURCE)));
1811
+ }
1523
1812
  }