@nylorun/runtime 0.26.0-beta → 0.27.0-beta

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 (321) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +18 -14
  3. package/dist/adapters/execution/restate.d.ts +15 -18
  4. package/dist/adapters/execution/restate.js +31 -27
  5. package/dist/adapters/replication/pgoutput.d.ts +1 -1
  6. package/dist/adapters/replication/pgoutput.js +3 -2
  7. package/dist/adapters/sandbox/local.js +8 -5
  8. package/dist/adapters/sandbox/virtual.js +2 -2
  9. package/dist/adapters/streams/s2.d.ts +1 -1
  10. package/dist/adapters/streams/s2.js +18 -14
  11. package/dist/api/a2a/card.js +1 -1
  12. package/dist/api/a2a/endpoint.js +3 -4
  13. package/dist/api/a2a/protocol.js +4 -0
  14. package/dist/api/a2a/routes.d.ts +2 -6
  15. package/dist/api/a2a/routes.js +12 -18
  16. package/dist/api/ag-ui/endpoint.js +2 -3
  17. package/dist/api/ag-ui/routes.js +4 -4
  18. package/dist/api/components.d.ts +326 -45
  19. package/dist/api/components.js +5 -1
  20. package/dist/api/gate/keys.js +7 -4
  21. package/dist/api/gate/routes.d.ts +6 -2
  22. package/dist/api/gate/routes.js +15 -11
  23. package/dist/api/http/body.d.ts +15 -3
  24. package/dist/api/http/body.js +15 -7
  25. package/dist/api/http/respond.js +6 -2
  26. package/dist/api/http/routes/access.js +5 -4
  27. package/dist/api/http/routes/artifacts.js +19 -7
  28. package/dist/api/http/routes/coverage.js +1 -1
  29. package/dist/api/http/routes/environment-manifests.js +1 -1
  30. package/dist/api/http/routes/environments.js +6 -6
  31. package/dist/api/http/routes/files.d.ts +2 -1
  32. package/dist/api/http/routes/files.js +31 -5
  33. package/dist/api/http/routes/mcp.js +1 -1
  34. package/dist/api/http/routes/reads.d.ts +35 -0
  35. package/dist/api/http/routes/reads.js +50 -22
  36. package/dist/api/http/routes/sessions.js +11 -10
  37. package/dist/api/http/routes/tenant.d.ts +2 -2
  38. package/dist/api/http/routes/tenant.js +69 -16
  39. package/dist/api/inbound-files.d.ts +3 -9
  40. package/dist/api/inbound-files.js +18 -24
  41. package/dist/api/openapi.js +6 -2
  42. package/dist/artifacts/disk.d.ts +0 -4
  43. package/dist/artifacts/disk.js +77 -73
  44. package/dist/artifacts/files.d.ts +13 -4
  45. package/dist/artifacts/files.js +27 -9
  46. package/dist/artifacts/folder-upload.d.ts +75 -0
  47. package/dist/artifacts/folder-upload.js +263 -0
  48. package/dist/artifacts/folders.d.ts +14 -8
  49. package/dist/artifacts/folders.js +19 -11
  50. package/dist/artifacts/links.js +7 -6
  51. package/dist/artifacts/parts.d.ts +9 -14
  52. package/dist/artifacts/parts.js +11 -9
  53. package/dist/artifacts/pipe.d.ts +15 -0
  54. package/dist/artifacts/pipe.js +71 -0
  55. package/dist/artifacts/quota.d.ts +36 -0
  56. package/dist/artifacts/quota.js +44 -0
  57. package/dist/artifacts/service.d.ts +25 -1
  58. package/dist/artifacts/service.js +70 -25
  59. package/dist/blob/fs.js +2 -1
  60. package/dist/blob/s3.d.ts +6 -2
  61. package/dist/blob/s3.js +9 -7
  62. package/dist/blob/types.d.ts +1 -1
  63. package/dist/blob/types.js +1 -1
  64. package/dist/core/limits.d.ts +11 -35
  65. package/dist/core/limits.js +5 -73
  66. package/dist/core/sandbox-routes.js +0 -1
  67. package/dist/core/turn-manifest.d.ts +1 -2
  68. package/dist/core/turn-manifest.js +4 -28
  69. package/dist/execution/memory.d.ts +8 -5
  70. package/dist/execution/memory.js +8 -6
  71. package/dist/execution/types.d.ts +1 -6
  72. package/dist/execution/types.js +0 -5
  73. package/dist/gates/contract.d.ts +3 -2
  74. package/dist/gates/contract.js +1 -1
  75. package/dist/gates/egress-credentials.js +4 -3
  76. package/dist/gates/egress.d.ts +9 -9
  77. package/dist/gates/egress.js +14 -16
  78. package/dist/gates/fixture-model.js +13 -2
  79. package/dist/gates/handler.d.ts +1 -3
  80. package/dist/gates/handler.js +4 -3
  81. package/dist/gates/http-client.d.ts +4 -3
  82. package/dist/gates/http-client.js +5 -7
  83. package/dist/gates/http-tool.d.ts +24 -4
  84. package/dist/gates/http-tool.js +40 -12
  85. package/dist/gates/in-process.d.ts +2 -1
  86. package/dist/gates/in-process.js +17 -9
  87. package/dist/gates/inflight.d.ts +2 -2
  88. package/dist/gates/inflight.js +3 -2
  89. package/dist/gates/journal.js +5 -7
  90. package/dist/gates/mcp-handler.d.ts +4 -3
  91. package/dist/gates/mcp-handler.js +15 -5
  92. package/dist/gates/model-gate.d.ts +11 -2
  93. package/dist/gates/policy.d.ts +28 -5
  94. package/dist/gates/policy.js +60 -17
  95. package/dist/gates/provider-gate.d.ts +22 -0
  96. package/dist/gates/provider-gate.js +30 -0
  97. package/dist/gates/tool-calls.d.ts +1 -3
  98. package/dist/gates/tool-calls.js +2 -3
  99. package/dist/gates/tool-client.d.ts +1 -1
  100. package/dist/gates/tool-client.js +5 -6
  101. package/dist/gates/tool-contract.d.ts +8 -5
  102. package/dist/gates/tool-contract.js +1 -1
  103. package/dist/gates/tool-gate.d.ts +18 -7
  104. package/dist/gates/tool-gate.js +49 -7
  105. package/dist/harness/calls.d.ts +27 -25
  106. package/dist/harness/calls.js +40 -46
  107. package/dist/harness/executors.d.ts +10 -3
  108. package/dist/harness/executors.js +53 -23
  109. package/dist/harness/main.js +2 -2
  110. package/dist/harness/pod.d.ts +0 -2
  111. package/dist/harness/pod.js +7 -6
  112. package/dist/harness/run-tokens.d.ts +4 -1
  113. package/dist/harness/run-tokens.js +6 -2
  114. package/dist/harness/service.d.ts +17 -14
  115. package/dist/harness/service.js +21 -35
  116. package/dist/harness/setup.d.ts +0 -2
  117. package/dist/harness/setup.js +4 -9
  118. package/dist/harness/ws-client.d.ts +2 -2
  119. package/dist/harness/ws-client.js +7 -5
  120. package/dist/harness/ws-port.d.ts +3 -4
  121. package/dist/harness/ws-port.js +3 -4
  122. package/dist/harness-api/artifacts.d.ts +20 -0
  123. package/dist/harness-api/artifacts.js +478 -0
  124. package/dist/harness-api/in-process.d.ts +2 -2
  125. package/dist/harness-api/in-process.js +18 -16
  126. package/dist/harness-api/record.d.ts +10 -9
  127. package/dist/harness-api/record.js +102 -157
  128. package/dist/harness-api/renew.d.ts +1 -1
  129. package/dist/harness-api/renew.js +2 -2
  130. package/dist/harness-api/server.d.ts +1 -16
  131. package/dist/harness-api/server.js +82 -73
  132. package/dist/harness-api/start.d.ts +20 -2
  133. package/dist/harness-api/start.js +57 -18
  134. package/dist/harness-api/tool-journal.d.ts +15 -10
  135. package/dist/harness-api/tool-journal.js +26 -24
  136. package/dist/harness-api/workspace.d.ts +0 -4
  137. package/dist/harness-api/workspace.js +2 -45
  138. package/dist/harness-api/ws-server.js +10 -10
  139. package/dist/host/create-host.js +2 -3
  140. package/dist/host/execution.js +2 -3
  141. package/dist/host/gates.d.ts +2 -9
  142. package/dist/host/gates.js +15 -12
  143. package/dist/host/http.d.ts +0 -2
  144. package/dist/host/http.js +0 -27
  145. package/dist/host/main.js +10 -3
  146. package/dist/host/operate.js +12 -3
  147. package/dist/host/stack-config.d.ts +11 -0
  148. package/dist/host/stack-config.js +21 -1
  149. package/dist/index.d.ts +0 -1
  150. package/dist/index.js +0 -1
  151. package/dist/infra/index.js +2 -1
  152. package/dist/infra/readiness.d.ts +0 -2
  153. package/dist/infra/readiness.js +4 -3
  154. package/dist/keys/client.d.ts +1 -0
  155. package/dist/keys/client.js +2 -3
  156. package/dist/keys/contract.d.ts +0 -2
  157. package/dist/keys/contract.js +0 -2
  158. package/dist/keys/egress-ca.d.ts +0 -2
  159. package/dist/keys/egress-ca.js +2 -3
  160. package/dist/keys/keys.js +6 -1
  161. package/dist/keys/x509.d.ts +8 -4
  162. package/dist/keys/x509.js +14 -5
  163. package/dist/management-openapi.json +1961 -994
  164. package/dist/mcp/connect.d.ts +35 -26
  165. package/dist/mcp/connect.js +112 -141
  166. package/dist/mcp/pool.d.ts +27 -22
  167. package/dist/mcp/pool.js +34 -48
  168. package/dist/mcp/preview.d.ts +12 -5
  169. package/dist/mcp/preview.js +36 -19
  170. package/dist/mcp/snapshot.d.ts +35 -86
  171. package/dist/mcp/snapshot.js +93 -135
  172. package/dist/model/auth-store.js +5 -3
  173. package/dist/model/capabilities.d.ts +1 -1
  174. package/dist/model/catalog.d.ts +9 -36
  175. package/dist/model/catalog.js +6 -33
  176. package/dist/model/descriptor.d.ts +51 -0
  177. package/dist/model/descriptor.js +83 -0
  178. package/dist/model/models.d.ts +0 -3
  179. package/dist/model/models.js +3 -5
  180. package/dist/model/native-blocks.js +1 -1
  181. package/dist/model/pi-model.d.ts +16 -5
  182. package/dist/model/pi-model.js +86 -105
  183. package/dist/openapi.json +838 -1218
  184. package/dist/reads/cursor.js +3 -1
  185. package/dist/record/append.d.ts +34 -2
  186. package/dist/record/append.js +50 -1
  187. package/dist/record/environment.d.ts +2 -0
  188. package/dist/record/environment.js +2 -0
  189. package/dist/record/index.d.ts +1 -1
  190. package/dist/record/index.js +1 -1
  191. package/dist/redact.d.ts +1 -1
  192. package/dist/redact.js +3 -2
  193. package/dist/sandbox/egress-token.d.ts +3 -3
  194. package/dist/sandbox/egress-token.js +3 -3
  195. package/dist/sandbox/join.js +3 -2
  196. package/dist/sandbox/manager.d.ts +41 -41
  197. package/dist/sandbox/manager.js +112 -44
  198. package/dist/sandbox/placement.d.ts +1 -1
  199. package/dist/sandbox/placement.js +5 -8
  200. package/dist/sandbox/pods/client.d.ts +1 -10
  201. package/dist/sandbox/pods/client.js +8 -4
  202. package/dist/sandbox/pods/lifecycle.d.ts +19 -11
  203. package/dist/sandbox/pods/lifecycle.js +45 -23
  204. package/dist/sandbox/pods/network-gate.d.ts +4 -5
  205. package/dist/sandbox/pods/network-gate.js +7 -7
  206. package/dist/sandbox/pods/reconcile.js +4 -1
  207. package/dist/sandbox/pods/spec.js +8 -5
  208. package/dist/sandbox/policy.d.ts +7 -1
  209. package/dist/sandbox/policy.js +10 -3
  210. package/dist/sandbox/resolve.d.ts +8 -3
  211. package/dist/sandbox/resolve.js +12 -2
  212. package/dist/sandbox/setup.d.ts +3 -1
  213. package/dist/sandbox/setup.js +34 -11
  214. package/dist/sandbox/share.d.ts +5 -14
  215. package/dist/sandbox/share.js +5 -34
  216. package/dist/sandbox/skills.d.ts +7 -13
  217. package/dist/sandbox/skills.js +16 -31
  218. package/dist/sandbox/tenant-config.d.ts +2 -2
  219. package/dist/sandbox/tenant-config.js +4 -3
  220. package/dist/sandbox/tools.d.ts +7 -1
  221. package/dist/sandbox/tools.js +26 -26
  222. package/dist/sandbox/types.d.ts +10 -3
  223. package/dist/sandbox/types.js +9 -0
  224. package/dist/store/postgres/connect.d.ts +6 -5
  225. package/dist/store/postgres/connect.js +14 -11
  226. package/dist/store/postgres/control.js +13 -13
  227. package/dist/store/postgres/drizzle/0028_artifact_write_keys.sql +2 -0
  228. package/dist/store/postgres/drizzle/0029_protocol_15.sql +11 -0
  229. package/dist/store/postgres/drizzle/0030_protocol_15_runtime_block.sql +11 -0
  230. package/dist/store/postgres/drizzle/0031_protocol_15_mcp_snapshot.sql +8 -0
  231. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  232. package/dist/store/postgres/reads.js +10 -2
  233. package/dist/store/postgres/schema.d.ts +19 -0
  234. package/dist/store/postgres/schema.js +7 -0
  235. package/dist/store/postgres/store.js +55 -26
  236. package/dist/store/types.d.ts +17 -7
  237. package/dist/streams/relay/core.d.ts +3 -3
  238. package/dist/streams/relay/core.js +9 -10
  239. package/dist/tenant/advance.js +21 -31
  240. package/dist/tenant/commands.js +9 -16
  241. package/dist/tenant/context.d.ts +14 -17
  242. package/dist/tenant/definition-files.d.ts +25 -4
  243. package/dist/tenant/definition-files.js +85 -47
  244. package/dist/tenant/effects.js +9 -36
  245. package/dist/tenant/environment-manifests.js +9 -1
  246. package/dist/tenant/environments.d.ts +0 -2
  247. package/dist/tenant/environments.js +49 -28
  248. package/dist/tenant/ephemeral.js +2 -1
  249. package/dist/tenant/history.d.ts +1 -1
  250. package/dist/tenant/history.js +1 -1
  251. package/dist/tenant/host-token.d.ts +3 -3
  252. package/dist/tenant/host-token.js +11 -10
  253. package/dist/tenant/http.d.ts +5 -8
  254. package/dist/tenant/http.js +9 -0
  255. package/dist/tenant/identity-file.d.ts +23 -0
  256. package/dist/tenant/identity-file.js +34 -6
  257. package/dist/tenant/issuers.d.ts +5 -5
  258. package/dist/tenant/issuers.js +31 -23
  259. package/dist/tenant/jwt.d.ts +8 -2
  260. package/dist/tenant/jwt.js +10 -2
  261. package/dist/tenant/limits.d.ts +90 -0
  262. package/dist/tenant/limits.js +175 -0
  263. package/dist/tenant/module.d.ts +4 -1
  264. package/dist/tenant/module.js +9 -8
  265. package/dist/tenant/outbound.d.ts +11 -14
  266. package/dist/tenant/outbound.js +10 -15
  267. package/dist/tenant/run-grants.js +3 -2
  268. package/dist/tenant/run-token.d.ts +0 -4
  269. package/dist/tenant/run-token.js +6 -11
  270. package/dist/tenant/runtime-limits.d.ts +24 -0
  271. package/dist/tenant/runtime-limits.js +27 -0
  272. package/dist/tenant/runtime.js +30 -43
  273. package/dist/tenant/session-streams.js +7 -6
  274. package/dist/tenant/session.d.ts +1 -1
  275. package/dist/tenant/session.js +1 -1
  276. package/dist/tenant/sessions.js +32 -29
  277. package/dist/tenant/signing-keys.d.ts +1 -1
  278. package/dist/tenant/signing-keys.js +5 -6
  279. package/dist/tenant/status.js +4 -3
  280. package/dist/tenant/streams.js +18 -20
  281. package/dist/tenant/sweep.d.ts +1 -21
  282. package/dist/tenant/sweep.js +39 -48
  283. package/dist/tenant/transcript.d.ts +70 -23
  284. package/dist/tenant/transcript.js +28 -22
  285. package/dist/tenant/types.d.ts +9 -14
  286. package/dist/tenant/worker.d.ts +2 -10
  287. package/dist/tenant/worker.js +5 -14
  288. package/dist/traces/ids.js +5 -1
  289. package/dist/traces/map.d.ts +9 -6
  290. package/dist/traces/map.js +9 -11
  291. package/dist/traces/otlp.d.ts +3 -3
  292. package/dist/traces/otlp.js +5 -3
  293. package/dist/traces/projector.d.ts +9 -4
  294. package/dist/traces/projector.js +13 -7
  295. package/dist/traces/read.js +6 -1
  296. package/dist/tuning.d.ts +383 -0
  297. package/dist/tuning.js +658 -0
  298. package/dist/vault/headers.js +4 -2
  299. package/dist/version.d.ts +1 -1
  300. package/dist/version.js +1 -1
  301. package/package.json +3 -3
  302. package/dist/artifacts/export.d.ts +0 -8
  303. package/dist/artifacts/export.js +0 -204
  304. package/dist/artifacts/media-types.d.ts +0 -30
  305. package/dist/artifacts/media-types.js +0 -137
  306. package/dist/artifacts/workspace.d.ts +0 -60
  307. package/dist/artifacts/workspace.js +0 -45
  308. package/dist/media.d.ts +0 -10
  309. package/dist/media.js +0 -58
  310. package/dist/sandbox/runtime-tools.d.ts +0 -9
  311. package/dist/sandbox/runtime-tools.js +0 -16
  312. package/dist/sandbox/session-sandbox.d.ts +0 -16
  313. package/dist/sandbox/session-sandbox.js +0 -77
  314. package/dist/tenant/artifact-tool.d.ts +0 -13
  315. package/dist/tenant/artifact-tool.js +0 -243
  316. package/dist/tenant/skill-tool.d.ts +0 -20
  317. package/dist/tenant/skill-tool.js +0 -67
  318. package/dist/tenant/tool-results.d.ts +0 -57
  319. package/dist/tenant/tool-results.js +0 -298
  320. package/dist/tenant/tool-search.d.ts +0 -39
  321. package/dist/tenant/tool-search.js +0 -103
@@ -0,0 +1,44 @@
1
+ import { limitsInForce, readTenantLimits } from "../tenant/limits.js";
2
+ /** The quota for an artifact of `environmentId`, or of the Tenant alone when undefined. */
3
+ export async function artifactQuota(t, environmentId) {
4
+ const tenant = await readTenantLimits(t);
5
+ const set = environmentId === undefined ? undefined : (await t.environment(environmentId))?.applied.limits;
6
+ const inForce = limitsInForce(tenant, set);
7
+ return {
8
+ fileBytes: inForce.artifacts.fileBytes,
9
+ totals: [
10
+ { limitBytes: tenant.artifacts.totalBytes },
11
+ ...(environmentId !== undefined && set?.artifacts?.totalBytes !== undefined
12
+ ? [{ environmentId, limitBytes: inForce.artifacts.totalBytes }]
13
+ : []),
14
+ ],
15
+ };
16
+ }
17
+ /** The bytes each of the quota's totals holds now. */
18
+ export async function artifactUsage(t, quota) {
19
+ const usage = [];
20
+ for (const total of quota.totals)
21
+ usage.push({
22
+ total,
23
+ usedBytes: await t.artifactBytes(total.environmentId === undefined ? undefined : { environmentId: total.environmentId }),
24
+ });
25
+ return usage;
26
+ }
27
+ /** The total with the least room left, and that room (none left: zero or less). */
28
+ export function tightest(usage) {
29
+ let best = usage[0];
30
+ for (const entry of usage)
31
+ if (entry.total.limitBytes - entry.usedBytes < best.total.limitBytes - best.usedBytes)
32
+ best = entry;
33
+ return { ...best, roomBytes: best.total.limitBytes - best.usedBytes };
34
+ }
35
+ /** Whose artifacts a total counts: `The Tenant's artifacts`, or `Environment <id>'s artifacts`. */
36
+ export function totalSubject(total) {
37
+ return total.environmentId === undefined ? "The Tenant's artifacts" : `Environment ${total.environmentId}'s artifacts`;
38
+ }
39
+ /** The Environment a session's artifacts belong to, if it names one. */
40
+ export async function environmentOfSession(t, sessionId) {
41
+ if (sessionId === null)
42
+ return undefined;
43
+ return (await t.get("sessions", sessionId))?.environmentId;
44
+ }
@@ -2,6 +2,7 @@ import type { ArtifactSource, ArtifactVersionView, ArtifactView, FolderManifest
2
2
  import { type BlobBody } from "../blob/index.js";
3
3
  import type { ArtifactRow, ArtifactVersionRow, Tx } from "../store/types.js";
4
4
  import { type SessionAccess, type TenantContext } from "../tenant/context.js";
5
+ import { type ArtifactQuota, type ArtifactUsage } from "./quota.js";
5
6
  /** What the caller may reach: undefined for the whole Tenant (an application principal). */
6
7
  export type ArtifactAccess = SessionAccess | undefined;
7
8
  export interface UploadInput {
@@ -17,15 +18,38 @@ export interface UploadInput {
17
18
  /** The body's declared length (`Content-Length`): one past the limits is refused unread. */
18
19
  readonly declaredBytes?: number;
19
20
  readonly source: ArtifactSource;
20
- /** The turn that saved it (`save_artifact`), for the event. */
21
+ /** The turn that saved it (a harness's `artifact.write`), for the event. */
21
22
  readonly turnId?: string | null;
22
23
  /** The tool call that saved it, for the event. */
23
24
  readonly callId?: string;
25
+ /**
26
+ * The idempotency key of a harness write (`writeKeyOf`): a version already stored with it is
27
+ * answered instead, and this upload's bytes are dropped. Needs `sessionId` or `artifactId`.
28
+ */
29
+ readonly writeKey?: string;
24
30
  }
25
31
  export interface Uploaded {
26
32
  readonly artifact: ArtifactView;
27
33
  readonly version: ArtifactVersionView;
34
+ /** False when `writeKey` named a version an earlier write stored. */
35
+ readonly created: boolean;
28
36
  }
37
+ /** The key a harness write of session `sessionId` is stored by: its key, scoped to the session. */
38
+ export declare function writeKeyOf(sessionId: string, key: string): string;
39
+ /** Why a write does not fit: the message, and the limit it passes. */
40
+ export interface ArtifactRefusal {
41
+ readonly message: string;
42
+ readonly limitBytes: number;
43
+ }
44
+ /**
45
+ * What one file may hold now within `quota`: the per-file cap, or what is left of the fullest
46
+ * total (`maxBytes`), and why a file of `size` bytes is refused (`refusal`; with no size, whether
47
+ * any room is left at all).
48
+ */
49
+ export declare function artifactRoom(quota: ArtifactQuota, usage: readonly ArtifactUsage[]): {
50
+ readonly maxBytes: number;
51
+ refusal(size?: number): ArtifactRefusal | undefined;
52
+ };
29
53
  export declare function versionView(row: ArtifactVersionRow): ArtifactVersionView;
30
54
  export declare function artifactView(row: ArtifactRow, versions?: ArtifactVersionRow[]): ArtifactView;
31
55
  /** An artifact name: 1–255 characters, no path separators or control characters. */
@@ -4,11 +4,12 @@
4
4
  * (F8.2) are written by the turn-end export (`./export.ts`, `./folders.ts`) and read here.
5
5
  *
6
6
  * - **Uploads stream.** The body goes to the store as it arrives, at a fresh random key, counted
7
- * against the per-file cap and what is left of the Tenant total (`maxBytes`): nothing is
8
- * buffered whole, and a body past either limit stores nothing (`413 limit_exceeded`).
7
+ * against the per-file cap and what is left of each total it counts towards (`maxBytes`,
8
+ * `./quota.ts`: the Tenant's, and a session's Environment's when its manifest sets one):
9
+ * nothing is buffered whole, and a body past a limit stores nothing (`413 limit_exceeded`).
9
10
  * - **Postgres is the commit point.** One transaction writes the version row and, for a session's
10
11
  * artifact, its `artifact.created` or `artifact.version.created` event through the record
11
- * module, after re-checking the Tenant total under the quota lock. Until it commits the object
12
+ * module, after re-checking each total under the quota lock. Until it commits the object
12
13
  * is garbage, and a failed commit deletes it.
13
14
  * - **Access mirrors sessions.** An application principal reaches every artifact. A request acting
14
15
  * for a person (a trusted issuer's token or subject headers) reaches only artifacts of that person's
@@ -16,24 +17,53 @@
16
17
  *
17
18
  * Lock order, the same in every write: the session, then the quota lock, then the artifact row.
18
19
  */
19
- import { randomUUID } from "node:crypto";
20
+ import { createHash, randomUUID } from "node:crypto";
20
21
  import { ARTIFACT_NAME_MAX } from "@nylorun/core/contracts";
21
22
  import { isArtifactId, newArtifactId } from "@nylorun/core/compatibility";
22
23
  import { BlobTooLargeError } from "../blob/index.js";
23
24
  import { lockedSession, sessionOf, } from "../tenant/context.js";
24
25
  import { fail } from "../tenant/http.js";
25
- import { readArtifactLimits } from "./config.js";
26
+ import { artifactQuota, artifactUsage, environmentOfSession, tightest, totalSubject, } from "./quota.js";
26
27
  import { collectContent, readManifest } from "./folders.js";
27
- import { contentTypeFor } from "./media-types.js";
28
+ import { contentTypeFor } from "@nylorun/core/contracts";
29
+ /** The key a harness write of session `sessionId` is stored by: its key, scoped to the session. */
30
+ export function writeKeyOf(sessionId, key) {
31
+ return createHash("sha256").update(`${sessionId}\n${key}`).digest("hex");
32
+ }
28
33
  const notFound = () => fail(404, "Artifact not found");
29
34
  function tooLarge(message, limit) {
30
35
  return fail(413, message, { code: "limit_exceeded", details: { limitBytes: limit } });
31
36
  }
32
- /** A body past `maxBytes`: past the per-file cap, or past what is left of the Tenant total. */
33
- function tooLargeFor(maxBytes, limits) {
34
- return maxBytes === limits.fileBytes
35
- ? tooLarge(`A file may hold at most ${limits.fileBytes} bytes`, limits.fileBytes)
36
- : tooLarge(`The Tenant's artifacts may hold at most ${limits.totalBytes} bytes; this file does not fit`, limits.totalBytes);
37
+ /**
38
+ * What one file may hold now within `quota`: the per-file cap, or what is left of the fullest
39
+ * total (`maxBytes`), and why a file of `size` bytes is refused (`refusal`; with no size, whether
40
+ * any room is left at all).
41
+ */
42
+ export function artifactRoom(quota, usage) {
43
+ const fullest = tightest(usage);
44
+ const maxBytes = Math.max(0, Math.min(quota.fileBytes, fullest.roomBytes));
45
+ return {
46
+ maxBytes,
47
+ refusal(size) {
48
+ if (fullest.roomBytes <= 0)
49
+ return {
50
+ message: `${totalSubject(fullest.total)} hold ${fullest.usedBytes} of ${fullest.total.limitBytes} bytes; delete some first`,
51
+ limitBytes: fullest.total.limitBytes,
52
+ };
53
+ if (size === undefined || size <= maxBytes)
54
+ return undefined;
55
+ if (maxBytes === quota.fileBytes)
56
+ return { message: `A file may hold at most ${quota.fileBytes} bytes`, limitBytes: quota.fileBytes };
57
+ return {
58
+ message: `${totalSubject(fullest.total)} may hold at most ${fullest.total.limitBytes} bytes; this file does not fit`,
59
+ limitBytes: fullest.total.limitBytes,
60
+ };
61
+ },
62
+ };
63
+ }
64
+ function refused(refusal) {
65
+ if (refusal)
66
+ tooLarge(refusal.message, refusal.limitBytes);
37
67
  }
38
68
  export function versionView(row) {
39
69
  return {
@@ -108,7 +138,7 @@ export async function versionOf(t, row, version) {
108
138
  export async function uploadArtifact(ctx, input, body, access, signal) {
109
139
  const name = input.artifactId === undefined ? checkArtifactName(input.name) : undefined;
110
140
  // Where it goes and how much room is left, before a byte is stored.
111
- const { limits, used, sessionId, fileName } = await ctx.store.tx(async (t) => {
141
+ const { quota, usage, sessionId, fileName } = await ctx.store.tx(async (t) => {
112
142
  let sessionId;
113
143
  let fileName = name;
114
144
  if (input.artifactId !== undefined) {
@@ -125,15 +155,15 @@ export async function uploadArtifact(ctx, input, body, access, signal) {
125
155
  await sessionOf(t, input.sessionId, access);
126
156
  sessionId = input.sessionId ?? null;
127
157
  }
128
- return { limits: await readArtifactLimits(t), used: await t.artifactBytes(), sessionId, fileName };
158
+ const quota = await artifactQuota(t, await environmentOfSession(t, sessionId));
159
+ return { quota, usage: await artifactUsage(t, quota), sessionId, fileName };
129
160
  });
130
161
  const contentType = input.contentType ?? contentTypeFor(fileName);
131
- const room = limits.totalBytes - used;
132
- if (room <= 0)
133
- tooLarge(`The Tenant's artifacts hold ${used} of ${limits.totalBytes} bytes; delete some first`, limits.totalBytes);
134
- const maxBytes = Math.min(limits.fileBytes, room);
135
- if (input.declaredBytes !== undefined && input.declaredBytes > maxBytes)
136
- tooLargeFor(maxBytes, limits);
162
+ const room = artifactRoom(quota, usage);
163
+ refused(room.refusal());
164
+ const { maxBytes } = room;
165
+ if (input.declaredBytes !== undefined)
166
+ refused(room.refusal(input.declaredBytes));
137
167
  const artifactId = input.artifactId ?? newArtifactId();
138
168
  const key = `artifacts/${artifactId}/${randomUUID()}`;
139
169
  let stored;
@@ -146,20 +176,29 @@ export async function uploadArtifact(ctx, input, body, access, signal) {
146
176
  }
147
177
  catch (error) {
148
178
  if (error instanceof BlobTooLargeError)
149
- tooLargeFor(maxBytes, limits);
179
+ refused(room.refusal(maxBytes + 1));
150
180
  throw error;
151
181
  }
152
182
  const createdAt = new Date().toISOString();
183
+ let uploaded;
153
184
  try {
154
- return await ctx.store.tx(async (t) => {
185
+ uploaded = await ctx.store.tx(async (t) => {
155
186
  // The session first, then the quota, then the artifact (the module's lock order).
156
187
  let session;
157
188
  if (sessionId !== null)
158
189
  session = await lockedSession(t, sessionId, access);
190
+ // A harness's write repeated: the session's lock orders it after the first, whose version
191
+ // it answers.
192
+ const known = input.writeKey === undefined ? undefined : await t.artifactVersionByWriteKey(input.writeKey);
193
+ if (known) {
194
+ const row = await t.artifact(known.artifactId);
195
+ if (row)
196
+ return { artifact: artifactView(row), version: versionView(known), created: false };
197
+ }
159
198
  await t.lockArtifactQuota();
160
- const total = await t.artifactBytes();
161
- if (total + stored.size > limits.totalBytes)
162
- tooLarge(`The Tenant's artifacts may hold at most ${limits.totalBytes} bytes; this file does not fit`, limits.totalBytes);
199
+ for (const { total, usedBytes } of await artifactUsage(t, quota))
200
+ if (usedBytes + stored.size > total.limitBytes)
201
+ tooLarge(`${totalSubject(total)} may hold at most ${total.limitBytes} bytes; this file does not fit`, total.limitBytes);
163
202
  let row;
164
203
  let version;
165
204
  if (input.artifactId !== undefined) {
@@ -173,6 +212,7 @@ export async function uploadArtifact(ctx, input, body, access, signal) {
173
212
  contentType,
174
213
  source: input.source,
175
214
  createdAt,
215
+ writeKey: input.writeKey ?? null,
176
216
  };
177
217
  await t.insertArtifactVersion(version);
178
218
  row = {
@@ -206,6 +246,7 @@ export async function uploadArtifact(ctx, input, body, access, signal) {
206
246
  contentType,
207
247
  source: input.source,
208
248
  createdAt,
249
+ writeKey: input.writeKey ?? null,
209
250
  };
210
251
  await t.insertArtifact(row, version);
211
252
  }
@@ -221,7 +262,7 @@ export async function uploadArtifact(ctx, input, body, access, signal) {
221
262
  source: version.source,
222
263
  ...(input.callId === undefined ? {} : { callId: input.callId }),
223
264
  });
224
- return { artifact: artifactView(row), version: versionView(version) };
265
+ return { artifact: artifactView(row), version: versionView(version), created: true };
225
266
  });
226
267
  }
227
268
  catch (error) {
@@ -229,6 +270,10 @@ export async function uploadArtifact(ctx, input, body, access, signal) {
229
270
  await ctx.blobs.delete(key).catch(() => undefined);
230
271
  throw error;
231
272
  }
273
+ // A repeated write stored nothing of its own: its object is garbage too.
274
+ if (!uploaded.created)
275
+ await ctx.blobs.delete(key).catch(() => undefined);
276
+ return uploaded;
232
277
  }
233
278
  /** The caller's artifacts: of one session, or of every session it reaches. */
234
279
  export async function listArtifacts(ctx, filter, access) {
package/dist/blob/fs.js CHANGED
@@ -2,7 +2,8 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { mkdir, open, readFile, readdir, rename, rm, stat, unlink } from "node:fs/promises";
3
3
  import { dirname, join, relative, sep } from "node:path";
4
4
  import { Readable } from "node:stream";
5
- import { BlobRangeError, BlobTooLargeError, DEFAULT_CONTENT_TYPE, assertBlobKey, assertBlobPrefix, assertMaxBytes, assertRange, chunksOf, } from "./types.js";
5
+ import { DEFAULT_CONTENT_TYPE } from "@nylorun/core/contracts";
6
+ import { BlobRangeError, BlobTooLargeError, assertBlobKey, assertBlobPrefix, assertMaxBytes, assertRange, chunksOf, } from "./types.js";
6
7
  function isMissing(error) {
7
8
  const code = error?.code;
8
9
  return code === "ENOENT" || code === "ENOTDIR";
package/dist/blob/s3.d.ts CHANGED
@@ -14,10 +14,14 @@ export interface S3BlobStoreOptions {
14
14
  forcePathStyle?: boolean;
15
15
  /**
16
16
  * A body longer than this is uploaded in parts of about this size (multipart upload); a
17
- * shorter one in one request. At least 5 MiB, the S3 minimum. Default 8 MiB.
17
+ * shorter one in one request. At least 5 MiB, the S3 minimum (`S3_MIN_PART_BYTES`). Default
18
+ * `objectStore.partSizeBytes` (Host tuning, 8 MiB).
18
19
  */
19
20
  partSize?: number;
20
- /** Attempts for a request that fails with a network error, 429 or 5xx. Default 3. */
21
+ /**
22
+ * Attempts for a request that fails with a network error, 429 or 5xx. Default
23
+ * `objectStore.attempts` (3); the wait after a failed one is `objectStore.retryBaseMs`, doubled.
24
+ */
21
25
  attempts?: number;
22
26
  /** Replaces the global `fetch` (tests). */
23
27
  fetch?: typeof fetch;
package/dist/blob/s3.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { AwsClient } from "aws4fetch";
3
- import { BlobRangeError, BlobStoreError, BlobTooLargeError, DEFAULT_CONTENT_TYPE, assertBlobKey, assertBlobPrefix, assertMaxBytes, assertRange, chunksOf, } from "./types.js";
4
- const MIN_PART_SIZE = 5 * 1024 * 1024;
5
- const DEFAULT_PART_SIZE = 8 * 1024 * 1024;
3
+ import { hostTuning, S3_MIN_PART_BYTES } from "../tuning.js";
4
+ import { DEFAULT_CONTENT_TYPE } from "@nylorun/core/contracts";
5
+ import { BlobRangeError, BlobStoreError, BlobTooLargeError, assertBlobKey, assertBlobPrefix, assertMaxBytes, assertRange, chunksOf, } from "./types.js";
6
6
  function sha256Hex(bytes) {
7
7
  return createHash("sha256").update(bytes).digest("hex");
8
8
  }
@@ -57,8 +57,10 @@ export function createS3BlobStore(options) {
57
57
  const bucketUrl = pathStyle
58
58
  ? `${base}/${options.bucket}`
59
59
  : `${endpoint.protocol}//${options.bucket}.${endpoint.host}${endpoint.pathname.replace(/\/+$/, "")}`;
60
- const partSize = Math.max(options.partSize ?? DEFAULT_PART_SIZE, MIN_PART_SIZE);
61
- const attempts = Math.max(1, options.attempts ?? 3);
60
+ const tuning = hostTuning().objectStore;
61
+ const partSize = Math.max(options.partSize ?? tuning.partSizeBytes, S3_MIN_PART_BYTES);
62
+ const attempts = Math.max(1, options.attempts ?? tuning.attempts);
63
+ const pause = (attempt) => new Promise((resolve) => setTimeout(resolve, tuning.retryBaseMs * 2 ** (attempt - 1)));
62
64
  const fetchImpl = options.fetch ?? globalThis.fetch;
63
65
  const client = new AwsClient({
64
66
  accessKeyId: options.accessKeyId,
@@ -81,7 +83,7 @@ export function createS3BlobStore(options) {
81
83
  const response = await fetchImpl(await client.sign(url, init));
82
84
  if ((response.status >= 500 || response.status === 429) && attempt < attempts && !once) {
83
85
  await response.body?.cancel();
84
- await new Promise((resolve) => setTimeout(resolve, 50 * 2 ** attempt));
86
+ await pause(attempt);
85
87
  continue;
86
88
  }
87
89
  return response;
@@ -91,7 +93,7 @@ export function createS3BlobStore(options) {
91
93
  throw error;
92
94
  lastError = error;
93
95
  if (attempt < (once ? 1 : attempts))
94
- await new Promise((resolve) => setTimeout(resolve, 50 * 2 ** attempt));
96
+ await pause(attempt);
95
97
  }
96
98
  }
97
99
  throw new BlobStoreError(`The Object store is unreachable: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
@@ -93,7 +93,7 @@ export declare class BlobRangeError extends Error {
93
93
  readonly size: number;
94
94
  constructor(range: BlobRange, size: number);
95
95
  }
96
- export declare const DEFAULT_CONTENT_TYPE = "application/octet-stream";
96
+ /** The longest object key S3 takes: 1024 bytes of UTF-8. A constant of the S3 API, not a setting. */
97
97
  export declare const MAX_KEY_BYTES = 1024;
98
98
  /**
99
99
  * Keys are `/`-separated segments of `A-Z a-z 0-9 . _ -`, neither `.` nor `..`, at most 1024
@@ -41,7 +41,7 @@ export class BlobRangeError extends Error {
41
41
  this.name = "BlobRangeError";
42
42
  }
43
43
  }
44
- export const DEFAULT_CONTENT_TYPE = "application/octet-stream";
44
+ /** The longest object key S3 takes: 1024 bytes of UTF-8. A constant of the S3 API, not a setting. */
45
45
  export const MAX_KEY_BYTES = 1024;
46
46
  const SEGMENT = /^[A-Za-z0-9._-]+$/;
47
47
  /**
@@ -1,40 +1,16 @@
1
1
  /**
2
- * Operator flow limits (`workflows.md` §13).
3
- * Host configuration only — never workflow API settings.
2
+ * Flow limits (`workflows.md` §13): the Tenant's Runtime limits `flowConcurrency`, `flowMapItems`
3
+ * and `flowLoopIterations` (`PUT /v1/tenant/limits`), read where they are checked. Core holds a
4
+ * flow turn to `flowConcurrency` when it dispatches effects; the flow engine checks the other two,
5
+ * which a flow's `turn.start` carries (`flowLimitsOf`).
4
6
  */
5
- export type FlowLimits = {
6
- readonly maxConcurrency: number;
7
+ import type { RuntimeLimits } from "@nylorun/core/contracts";
8
+ /** What a flow's `turn.start.options.flowLimits` carries to the flow engine. */
9
+ export interface FlowEngineLimits {
7
10
  readonly maxMapItems: number;
8
11
  readonly maxLoopIterations: number;
9
- };
10
- export declare const FLOW_LIMIT_DEFAULTS: FlowLimits;
11
- export declare const FLOW_LIMIT_ENV: {
12
- readonly maxConcurrency: "NYLORUN_FLOW_MAX_CONCURRENCY";
13
- readonly maxMapItems: "NYLORUN_FLOW_MAX_MAP_ITEMS";
14
- readonly maxLoopIterations: "NYLORUN_FLOW_MAX_LOOP_ITERATIONS";
15
- };
16
- export type FlowLimitErrorCode = "map.too-many-items" | "loop.too-many-iterations";
17
- export declare class FlowLimitError extends Error {
18
- readonly code: FlowLimitErrorCode;
19
- readonly path?: string;
20
- constructor(code: FlowLimitErrorCode, message: string, path?: string);
21
12
  }
22
- /**
23
- * Resolve limits from `RuntimeOptions.flow` overrides and optional env snapshot.
24
- * Does not read ambient process environment — callers pass an explicit env map.
25
- */
26
- export declare function resolveFlowLimits(input?: {
27
- readonly flow?: Partial<FlowLimits> | null;
28
- readonly env?: Readonly<Record<string, string | undefined>>;
29
- }): FlowLimits;
30
- /** Further ready work waits when active >= maxConcurrency (not an error). */
31
- export declare function mayDispatchMore(activeCount: number, limits: Pick<FlowLimits, "maxConcurrency">): boolean;
32
- /**
33
- * Map item ceiling. Exceeding raises `map.too-many-items` and nothing should start.
34
- */
35
- export declare function assertMapItemCount(count: number, limits: Pick<FlowLimits, "maxMapItems">, path?: string): void;
36
- /**
37
- * Loop operator ceiling. Exceeding raises `loop.too-many-iterations`.
38
- * `n` is the 1-based iteration about to run.
39
- */
40
- export declare function assertLoopIteration(n: number, limits: Pick<FlowLimits, "maxLoopIterations">, path?: string): void;
13
+ /** The flow engine's limits from the Tenant's Runtime limits. */
14
+ export declare function flowLimitsOf(limits: Pick<RuntimeLimits, "flowMapItems" | "flowLoopIterations">): FlowEngineLimits;
15
+ /** Further ready work waits when `active` reaches `flowConcurrency` (not an error). */
16
+ export declare function mayDispatchMore(activeCount: number, limits: Pick<RuntimeLimits, "flowConcurrency">): boolean;
@@ -1,76 +1,8 @@
1
- /**
2
- * Operator flow limits (`workflows.md` §13).
3
- * Host configuration only — never workflow API settings.
4
- */
5
- export const FLOW_LIMIT_DEFAULTS = {
6
- maxConcurrency: 8,
7
- maxMapItems: 1000,
8
- maxLoopIterations: 100,
9
- };
10
- export const FLOW_LIMIT_ENV = {
11
- maxConcurrency: "NYLORUN_FLOW_MAX_CONCURRENCY",
12
- maxMapItems: "NYLORUN_FLOW_MAX_MAP_ITEMS",
13
- maxLoopIterations: "NYLORUN_FLOW_MAX_LOOP_ITERATIONS",
14
- };
15
- export class FlowLimitError extends Error {
16
- code;
17
- path;
18
- constructor(code, message, path) {
19
- super(message);
20
- this.name = "FlowLimitError";
21
- this.code = code;
22
- if (path !== undefined)
23
- this.path = path;
24
- }
1
+ /** The flow engine's limits from the Tenant's Runtime limits. */
2
+ export function flowLimitsOf(limits) {
3
+ return { maxMapItems: limits.flowMapItems, maxLoopIterations: limits.flowLoopIterations };
25
4
  }
26
- function parsePositiveInt(raw, field) {
27
- if (raw === undefined || raw === "")
28
- return undefined;
29
- const n = Number(raw);
30
- if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0)
31
- throw new Error(`${field} must be a positive integer`);
32
- return n;
33
- }
34
- /**
35
- * Resolve limits from `RuntimeOptions.flow` overrides and optional env snapshot.
36
- * Does not read ambient process environment — callers pass an explicit env map.
37
- */
38
- export function resolveFlowLimits(input) {
39
- const env = input?.env ?? {};
40
- const flow = input?.flow ?? {};
41
- const fromEnv = {
42
- maxConcurrency: parsePositiveInt(env[FLOW_LIMIT_ENV.maxConcurrency], FLOW_LIMIT_ENV.maxConcurrency),
43
- maxMapItems: parsePositiveInt(env[FLOW_LIMIT_ENV.maxMapItems], FLOW_LIMIT_ENV.maxMapItems),
44
- maxLoopIterations: parsePositiveInt(env[FLOW_LIMIT_ENV.maxLoopIterations], FLOW_LIMIT_ENV.maxLoopIterations),
45
- };
46
- const pick = (field, override, envValue) => {
47
- const value = override ?? envValue ?? FLOW_LIMIT_DEFAULTS[field];
48
- if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0)
49
- throw new Error(`flow.${field} must be a positive integer`);
50
- return value;
51
- };
52
- return {
53
- maxConcurrency: pick("maxConcurrency", flow.maxConcurrency, fromEnv.maxConcurrency),
54
- maxMapItems: pick("maxMapItems", flow.maxMapItems, fromEnv.maxMapItems),
55
- maxLoopIterations: pick("maxLoopIterations", flow.maxLoopIterations, fromEnv.maxLoopIterations),
56
- };
57
- }
58
- /** Further ready work waits when active >= maxConcurrency (not an error). */
5
+ /** Further ready work waits when `active` reaches `flowConcurrency` (not an error). */
59
6
  export function mayDispatchMore(activeCount, limits) {
60
- return activeCount < limits.maxConcurrency;
61
- }
62
- /**
63
- * Map item ceiling. Exceeding raises `map.too-many-items` and nothing should start.
64
- */
65
- export function assertMapItemCount(count, limits, path) {
66
- if (count > limits.maxMapItems)
67
- throw new FlowLimitError("map.too-many-items", `Map produced ${count} items; maxMapItems is ${limits.maxMapItems}`, path);
68
- }
69
- /**
70
- * Loop operator ceiling. Exceeding raises `loop.too-many-iterations`.
71
- * `n` is the 1-based iteration about to run.
72
- */
73
- export function assertLoopIteration(n, limits, path) {
74
- if (n > limits.maxLoopIterations)
75
- throw new FlowLimitError("loop.too-many-iterations", `Loop iteration ${n} exceeds maxLoopIterations ${limits.maxLoopIterations}`, path);
7
+ return activeCount < limits.flowConcurrency;
76
8
  }
@@ -38,7 +38,6 @@ async function runTool(deps, session, tool, input, signal) {
38
38
  const ref = {
39
39
  id: workspace.ownerId,
40
40
  activeTurnId: session.activeTurnId,
41
- manifest: session.manifest,
42
41
  ...(workspace.environmentId === undefined ? {} : { environmentId: workspace.environmentId }),
43
42
  };
44
43
  return deps.sandbox.run(ref, spec, tool, input, signal);
@@ -17,8 +17,7 @@ export type TurnManifestErr = {
17
17
  };
18
18
  /**
19
19
  * Parse `message.manifest`, require it to be a variant of the session pin, and store the
20
- * manifest the turn runs with: the variant with the pin's `runtime` block (a variant never sets
21
- * one), by its hash. Applies to that turn only (caller must not latch).
20
+ * manifest the turn runs with, by its hash. Applies to that turn only (caller must not latch).
22
21
  */
23
22
  export declare function pinTurnManifest(args: {
24
23
  readonly candidate: unknown;
@@ -1,30 +1,9 @@
1
1
  import { AgentManifestSchema, } from "@nylorun/core/contracts";
2
2
  import { hashManifest } from "@nylorun/core/compatibility";
3
3
  import { isVariantOf } from "@nylorun/core/define";
4
- /**
5
- * The candidate's agents used as tools with the `runtime` block the pin gave each, when the
6
- * candidate left it out: a turn variant is written from the definition, and only the Runtime
7
- * writes `runtime`. A candidate that sets one keeps it, and must then match the pin's.
8
- */
9
- function withPinnedDelegates(candidate, pinned) {
10
- if (candidate.tools === undefined)
11
- return candidate;
12
- const pinnedAgents = new Map((pinned.tools ?? []).flatMap((tool) => tool.agent !== undefined && !("kind" in tool.agent) ? [[tool.name, tool.agent]] : []));
13
- let changed = false;
14
- const tools = candidate.tools.map((tool) => {
15
- const agent = tool.agent;
16
- const prior = pinnedAgents.get(tool.name);
17
- if (agent === undefined || "kind" in agent || agent.runtime !== undefined || prior?.runtime === undefined)
18
- return tool;
19
- changed = true;
20
- return { ...tool, agent: { ...agent, runtime: prior.runtime } };
21
- });
22
- return changed ? { ...candidate, tools } : candidate;
23
- }
24
4
  /**
25
5
  * Parse `message.manifest`, require it to be a variant of the session pin, and store the
26
- * manifest the turn runs with: the variant with the pin's `runtime` block (a variant never sets
27
- * one), by its hash. Applies to that turn only (caller must not latch).
6
+ * manifest the turn runs with, by its hash. Applies to that turn only (caller must not latch).
28
7
  */
29
8
  export function pinTurnManifest(args) {
30
9
  const parsed = AgentManifestSchema.safeParse(args.candidate);
@@ -35,17 +14,14 @@ export function pinTurnManifest(args) {
35
14
  message: "Turn manifest failed AgentManifestSchema validation",
36
15
  };
37
16
  }
38
- const candidate = withPinnedDelegates(parsed.data, args.pinned);
39
- if (!isVariantOf(candidate, args.pinned)) {
17
+ const manifest = parsed.data;
18
+ if (!isVariantOf(manifest, args.pinned)) {
40
19
  return {
41
20
  ok: false,
42
21
  code: "loop.invalid-agent",
43
- message: candidate.runtime === undefined
44
- ? "Turn manifest is not a variant of the session's pinned manifest"
45
- : "Turn manifest sets runtime, which only the Runtime writes: leave it out, and the turn runs with the session's",
22
+ message: "Turn manifest is not a variant of the session's pinned manifest",
46
23
  };
47
24
  }
48
- const manifest = args.pinned.runtime === undefined ? candidate : { ...candidate, runtime: args.pinned.runtime };
49
25
  const hash = hashManifest(manifest);
50
26
  args.store.put(hash, manifest);
51
27
  return { ok: true, manifest, hash };
@@ -1,16 +1,19 @@
1
1
  import { type DurableExecution, type SandboxSignal, type Wake, type WorkerHandlers } from "./types.js";
2
2
  export interface MemoryExecutionOptions {
3
- /** Delay between sweep passes of one Tenant. Default 5000. */
3
+ /** Delay between sweep passes of one Tenant. Default `sweep.intervalMs` (Host tuning, 5 s). */
4
4
  sweepIntervalMs?: number;
5
- /** First retry delay after `advance` throws; doubles per attempt. Default 50. */
5
+ /**
6
+ * First retry delay after `advance` throws; doubles per attempt. Default
7
+ * `execution.memoryRetryMs` (50 ms).
8
+ */
6
9
  retryDelayMs?: number;
7
- /** Attempts before giving up on a throwing handler and reporting it. Default 5. */
10
+ /** Attempts before giving up on a throwing handler and reporting it. Default `execution.memoryAttempts` (5). */
8
11
  maxAttempts?: number;
9
- /** How long a dedupe key merges repeated wakes. Default 24 hours. */
12
+ /** How long a dedupe key merges repeated wakes. Default `execution.dedupeRetentionMs` (24 hours). */
10
13
  dedupeRetentionMs?: number;
11
14
  /**
12
15
  * How long `stop` waits for running handlers after aborting them; the ones still running
13
- * then are abandoned. Default `DEFAULT_STOP_GRACE_MS`.
16
+ * then are abandoned. Default `execution.stopGraceMs` (30 s).
14
17
  */
15
18
  stopGraceMs?: number;
16
19
  /** Receives handler failures that are not retried. */
@@ -1,4 +1,5 @@
1
- import { DEFAULT_STOP_GRACE_MS, WAKE_REASONS, sessionKey, settleWithin, } from "./types.js";
1
+ import { WAKE_REASONS, sessionKey, settleWithin, } from "./types.js";
2
+ import { hostTuning } from "../tuning.js";
2
3
  /**
3
4
  * In-process `DurableExecution`. Not durable: wakes, timers and sweeps live in
4
5
  * memory. It keeps the seam's ordering guarantees within one process, which is
@@ -21,11 +22,12 @@ export class MemoryExecution {
21
22
  stopGraceMs;
22
23
  onError;
23
24
  constructor(options = {}) {
24
- this.sweepIntervalMs = options.sweepIntervalMs ?? 5000;
25
- this.retryDelayMs = options.retryDelayMs ?? 50;
26
- this.maxAttempts = options.maxAttempts ?? 5;
27
- this.dedupeRetentionMs = options.dedupeRetentionMs ?? 24 * 60 * 60_000;
28
- this.stopGraceMs = options.stopGraceMs ?? DEFAULT_STOP_GRACE_MS;
25
+ const tuning = hostTuning();
26
+ this.sweepIntervalMs = options.sweepIntervalMs ?? tuning.sweep.intervalMs;
27
+ this.retryDelayMs = options.retryDelayMs ?? tuning.execution.memoryRetryMs;
28
+ this.maxAttempts = options.maxAttempts ?? tuning.execution.memoryAttempts;
29
+ this.dedupeRetentionMs = options.dedupeRetentionMs ?? tuning.execution.dedupeRetentionMs;
30
+ this.stopGraceMs = options.stopGraceMs ?? tuning.execution.stopGraceMs;
29
31
  this.onError =
30
32
  options.onError ??
31
33
  ((error) => queueMicrotask(() => {