effect 4.1.1 → 4.1.2

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 (142) hide show
  1. package/ai-docs/package.json +2 -2
  2. package/dist/ByteSize.d.ts.map +1 -1
  3. package/dist/ByteSize.js +27 -14
  4. package/dist/ByteSize.js.map +1 -1
  5. package/dist/Iterable.d.ts.map +1 -1
  6. package/dist/Iterable.js +27 -24
  7. package/dist/Iterable.js.map +1 -1
  8. package/dist/JsonSchema.d.ts.map +1 -1
  9. package/dist/JsonSchema.js +27 -6
  10. package/dist/JsonSchema.js.map +1 -1
  11. package/dist/MutableRef.d.ts.map +1 -1
  12. package/dist/MutableRef.js +0 -7
  13. package/dist/MutableRef.js.map +1 -1
  14. package/dist/Ref.d.ts.map +1 -1
  15. package/dist/Ref.js +0 -8
  16. package/dist/Ref.js.map +1 -1
  17. package/dist/Schedule.d.ts.map +1 -1
  18. package/dist/Schedule.js +8 -3
  19. package/dist/Schedule.js.map +1 -1
  20. package/dist/Stream.d.ts.map +1 -1
  21. package/dist/Stream.js +6 -5
  22. package/dist/Stream.js.map +1 -1
  23. package/dist/ai/McpServer.d.ts.map +1 -1
  24. package/dist/ai/McpServer.js +61 -33
  25. package/dist/ai/McpServer.js.map +1 -1
  26. package/dist/ai/internal/mcpProtocol/v2026_07_28.js +4 -3
  27. package/dist/ai/internal/mcpProtocol/v2026_07_28.js.map +1 -1
  28. package/dist/ai/internal/structured-output.js +11 -2
  29. package/dist/ai/internal/structured-output.js.map +1 -1
  30. package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
  31. package/dist/cluster/ClusterWorkflowEngine.js +18 -9
  32. package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
  33. package/dist/cluster/Entity.d.ts +26 -0
  34. package/dist/cluster/Entity.d.ts.map +1 -1
  35. package/dist/cluster/Entity.js.map +1 -1
  36. package/dist/cluster/MessageStorage.d.ts +6 -6
  37. package/dist/cluster/MessageStorage.d.ts.map +1 -1
  38. package/dist/cluster/MessageStorage.js +1 -3
  39. package/dist/cluster/MessageStorage.js.map +1 -1
  40. package/dist/cluster/Runners.d.ts.map +1 -1
  41. package/dist/cluster/Runners.js +13 -2
  42. package/dist/cluster/Runners.js.map +1 -1
  43. package/dist/cluster/Sharding.d.ts +13 -0
  44. package/dist/cluster/Sharding.d.ts.map +1 -1
  45. package/dist/cluster/Sharding.js.map +1 -1
  46. package/dist/cluster/internal/clusterAbandon.d.ts.map +1 -1
  47. package/dist/cluster/internal/clusterAbandon.js +6 -4
  48. package/dist/cluster/internal/clusterAbandon.js.map +1 -1
  49. package/dist/cluster/internal/entityManager.js +48 -4
  50. package/dist/cluster/internal/entityManager.js.map +1 -1
  51. package/dist/cluster/internal/transactionNotifications.d.ts +5 -0
  52. package/dist/cluster/internal/transactionNotifications.d.ts.map +1 -0
  53. package/dist/cluster/internal/transactionNotifications.js +5 -0
  54. package/dist/cluster/internal/transactionNotifications.js.map +1 -0
  55. package/dist/encoding/Json5.d.ts +44 -0
  56. package/dist/encoding/Json5.d.ts.map +1 -0
  57. package/dist/encoding/Json5.js +56 -0
  58. package/dist/encoding/Json5.js.map +1 -0
  59. package/dist/encoding/Jsonc.d.ts +47 -0
  60. package/dist/encoding/Jsonc.d.ts.map +1 -0
  61. package/dist/encoding/Jsonc.js +120 -0
  62. package/dist/encoding/Jsonc.js.map +1 -0
  63. package/dist/encoding/SchemaBinary.d.ts +94 -0
  64. package/dist/encoding/SchemaBinary.d.ts.map +1 -1
  65. package/dist/encoding/SchemaBinary.js +219 -6
  66. package/dist/encoding/SchemaBinary.js.map +1 -1
  67. package/dist/encoding/index.d.ts +10 -0
  68. package/dist/encoding/index.d.ts.map +1 -1
  69. package/dist/encoding/index.js +10 -0
  70. package/dist/encoding/index.js.map +1 -1
  71. package/dist/eventlog/EventLogServer.d.ts.map +1 -1
  72. package/dist/eventlog/EventLogServer.js +28 -12
  73. package/dist/eventlog/EventLogServer.js.map +1 -1
  74. package/dist/http/HttpClient.d.ts +15 -9
  75. package/dist/http/HttpClient.d.ts.map +1 -1
  76. package/dist/http/HttpClient.js +18 -11
  77. package/dist/http/HttpClient.js.map +1 -1
  78. package/dist/internal/dateTime.js +17 -3
  79. package/dist/internal/dateTime.js.map +1 -1
  80. package/dist/internal/jsonParser.d.ts +5 -0
  81. package/dist/internal/jsonParser.d.ts.map +1 -0
  82. package/dist/internal/jsonParser.js +280 -0
  83. package/dist/internal/jsonParser.js.map +1 -0
  84. package/dist/internal/persistence.js +8 -1
  85. package/dist/internal/persistence.js.map +1 -1
  86. package/dist/internal/sql.js +16 -0
  87. package/dist/internal/sql.js.map +1 -1
  88. package/dist/internal/version.js +1 -1
  89. package/dist/observability/OtlpExporter.d.ts +4 -0
  90. package/dist/observability/OtlpExporter.d.ts.map +1 -1
  91. package/dist/observability/OtlpExporter.js +40 -12
  92. package/dist/observability/OtlpExporter.js.map +1 -1
  93. package/dist/persistence/RateLimiter.d.ts +7 -0
  94. package/dist/persistence/RateLimiter.d.ts.map +1 -1
  95. package/dist/persistence/RateLimiter.js +13 -4
  96. package/dist/persistence/RateLimiter.js.map +1 -1
  97. package/dist/rpc/RpcServer.d.ts.map +1 -1
  98. package/dist/rpc/RpcServer.js +10 -1
  99. package/dist/rpc/RpcServer.js.map +1 -1
  100. package/dist/sql/SqlClient.d.ts +8 -0
  101. package/dist/sql/SqlClient.d.ts.map +1 -1
  102. package/dist/sql/SqlClient.js +36 -12
  103. package/dist/sql/SqlClient.js.map +1 -1
  104. package/dist/sql/Statement.d.ts +5 -0
  105. package/dist/sql/Statement.d.ts.map +1 -1
  106. package/dist/sql/Statement.js +8 -6
  107. package/dist/sql/Statement.js.map +1 -1
  108. package/package.json +4 -4
  109. package/src/ByteSize.ts +29 -14
  110. package/src/Iterable.ts +20 -14
  111. package/src/JsonSchema.ts +45 -7
  112. package/src/MutableRef.ts +1 -8
  113. package/src/Ref.ts +0 -8
  114. package/src/Schedule.ts +8 -6
  115. package/src/Stream.ts +6 -5
  116. package/src/ai/McpServer.ts +66 -43
  117. package/src/ai/internal/mcpProtocol/v2026_07_28.ts +4 -5
  118. package/src/ai/internal/structured-output.ts +10 -4
  119. package/src/cluster/ClusterWorkflowEngine.ts +29 -28
  120. package/src/cluster/Entity.ts +52 -0
  121. package/src/cluster/MessageStorage.ts +3 -6
  122. package/src/cluster/Runners.ts +16 -2
  123. package/src/cluster/Sharding.ts +13 -0
  124. package/src/cluster/internal/clusterAbandon.ts +7 -7
  125. package/src/cluster/internal/entityManager.ts +65 -5
  126. package/src/cluster/internal/transactionNotifications.ts +7 -0
  127. package/src/encoding/Json5.ts +59 -0
  128. package/src/encoding/Jsonc.ts +126 -0
  129. package/src/encoding/SchemaBinary.ts +228 -7
  130. package/src/encoding/index.ts +12 -0
  131. package/src/eventlog/EventLogServer.ts +36 -12
  132. package/src/http/HttpClient.ts +34 -22
  133. package/src/internal/dateTime.ts +26 -3
  134. package/src/internal/jsonParser.ts +307 -0
  135. package/src/internal/persistence.ts +9 -1
  136. package/src/internal/sql.ts +25 -0
  137. package/src/internal/version.ts +1 -1
  138. package/src/observability/OtlpExporter.ts +42 -18
  139. package/src/persistence/RateLimiter.ts +13 -4
  140. package/src/rpc/RpcServer.ts +12 -1
  141. package/src/sql/SqlClient.ts +48 -14
  142. package/src/sql/Statement.ts +8 -13
package/src/JsonSchema.ts CHANGED
@@ -506,9 +506,23 @@ export function fromSchemaOpenApi3_0(schema: JsonSchema): Document<"draft-2020-1
506
506
  * @since 4.0.0
507
507
  */
508
508
  export function toDocumentDraft07(document: Document<"draft-2020-12">): Document<"draft-07"> {
509
+ return toDocumentDraft07WithDefinitionsPath(document)
510
+ }
511
+
512
+ /**
513
+ * Converts to Draft-07 with definitions located at `definitionsPath`, such as
514
+ * `["components", "schemas"]` after {@link toMultiDocumentComponents}, so
515
+ * references into them stay in place. Defaults to `$defs` → `definitions`.
516
+ *
517
+ * @internal
518
+ */
519
+ export function toDocumentDraft07WithDefinitionsPath(
520
+ document: Document<"draft-2020-12">,
521
+ definitionsPath?: ReadonlyArray<string>
522
+ ): Document<"draft-07"> {
509
523
  return {
510
524
  dialect: "draft-07",
511
- ...convertDocument(document, draft07Adapter)
525
+ ...convertDocument(document, draft07Adapter, undefined, definitionsPath)
512
526
  }
513
527
  }
514
528
 
@@ -620,6 +634,25 @@ export function toDocumentDraft04(document: Document<"draft-2020-12">): Document
620
634
  * @since 4.0.0
621
635
  */
622
636
  export function toMultiDocumentOpenApi3_1(multiDocument: MultiDocument<"draft-2020-12">): MultiDocument<"openapi-3.1"> {
637
+ return {
638
+ dialect: "openapi-3.1",
639
+ ...toMultiDocumentComponents(multiDocument, {
640
+ target: "OpenAPI 3.1",
641
+ checkSchema(schema) {
642
+ rejectKeywordCollisions(schema, OPEN_API_31_TARGET_COLLISIONS, "OpenAPI 3.1", "Draft 2020-12")
643
+ }
644
+ })
645
+ }
646
+ }
647
+
648
+ /** @internal */
649
+ export function toMultiDocumentComponents(
650
+ multiDocument: Pick<MultiDocument<"draft-2020-12">, "schemas" | "definitions">,
651
+ options: {
652
+ readonly target: string
653
+ readonly checkSchema?: ((schema: JsonSchema) => void) | undefined
654
+ }
655
+ ): Pick<MultiDocument<"draft-2020-12">, "schemas" | "definitions"> {
623
656
  const definitionKeys = Object.keys(multiDocument.definitions)
624
657
  const keyMap = new Map<string, string>()
625
658
  const usedKeys = new Set(definitionKeys.filter((key) => VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP.test(key)))
@@ -649,7 +682,7 @@ export function toMultiDocumentOpenApi3_1(multiDocument: MultiDocument<"draft-20
649
682
  const isRootResource = createsResource(schema.$id)
650
683
  const localDefinitions = Predicate.isObject(schema.$defs) ? schema.$defs : undefined
651
684
  return transformSchema(schema, (schema, inEmbeddedResource) => {
652
- rejectKeywordCollisions(schema, OPEN_API_31_TARGET_COLLISIONS, "OpenAPI 3.1", "Draft 2020-12")
685
+ options.checkSchema?.(schema)
653
686
  rewriteSchemaRef(schema, (reference, keyword) => {
654
687
  const path = reference.startsWith("#") ? parseUriFragment(reference) : undefined
655
688
  if (path === undefined || path[0] !== "$defs" || path.length < 2) return reference
@@ -661,7 +694,7 @@ export function toMultiDocumentOpenApi3_1(multiDocument: MultiDocument<"draft-20
661
694
  Object.hasOwn(multiDocument.definitions, key) &&
662
695
  (localDefinitions === undefined || !Object.hasOwn(localDefinitions, key))
663
696
  ) {
664
- unsupported(keyword, "OpenAPI 3.1", "a schema resource cannot reference the shared definitions pool")
697
+ unsupported(keyword, options.target, "a schema resource cannot reference the shared definitions pool")
665
698
  }
666
699
  return reference
667
700
  }
@@ -673,7 +706,7 @@ export function toMultiDocumentOpenApi3_1(multiDocument: MultiDocument<"draft-20
673
706
  }
674
707
 
675
708
  const schemas = multiDocument.schemas.map((schema) => rewrite(schema, true)) as unknown as MultiDocument<
676
- "openapi-3.1"
709
+ "draft-2020-12"
677
710
  >["schemas"]
678
711
  const definitions: Definitions = {}
679
712
  for (const key of definitionKeys) {
@@ -685,7 +718,6 @@ export function toMultiDocumentOpenApi3_1(multiDocument: MultiDocument<"draft-20
685
718
  }
686
719
 
687
720
  return {
688
- dialect: "openapi-3.1",
689
721
  schemas,
690
722
  definitions
691
723
  }
@@ -957,13 +989,19 @@ function convertSchema(root: JsonSchema, adapter: Adapter, options?: ConverterOp
957
989
  function convertDocument(
958
990
  document: Document<"draft-2020-12">,
959
991
  adapter: Adapter,
960
- options?: ConverterOptions
992
+ options?: ConverterOptions,
993
+ definitionsPath?: Path
961
994
  ): { readonly schema: JsonSchema; readonly definitions: Definitions } {
962
995
  return runConverter(adapter, { ...options, trackIds: true }, (convert) => ({
963
996
  schema: convert(document.schema) as JsonSchema,
964
997
  definitions: mapObject(
965
998
  document.definitions,
966
- (definition, key) => convert(definition, ["$defs", key], ["definitions", key]) as JsonSchema
999
+ (definition, key) =>
1000
+ convert(
1001
+ definition,
1002
+ [...(definitionsPath ?? ["$defs"]), key],
1003
+ [...(definitionsPath ?? ["definitions"]), key]
1004
+ ) as JsonSchema
967
1005
  ) as Definitions
968
1006
  }))
969
1007
  }
package/src/MutableRef.ts CHANGED
@@ -12,7 +12,6 @@
12
12
  */
13
13
  import * as Equal from "./Equal.ts"
14
14
  import * as Dual from "./Function.ts"
15
- import * as Hash from "./Hash.ts"
16
15
  import { type Inspectable, toJson } from "./Inspectable.ts"
17
16
  import { PipeInspectableProto } from "./internal/core.ts"
18
17
  import type { Pipeable } from "./Pipeable.ts"
@@ -75,15 +74,9 @@ export interface MutableRef<out T> extends Pipeable, Inspectable {
75
74
  current: T
76
75
  }
77
76
 
78
- const MutableRefProto: Omit<MutableRef<unknown>, "current"> & Equal.Equal = {
77
+ const MutableRefProto: Omit<MutableRef<unknown>, "current"> = {
79
78
  [TypeId]: TypeId,
80
79
  ...PipeInspectableProto,
81
- [Equal.symbol](that: unknown): boolean {
82
- return this === that
83
- },
84
- [Hash.symbol](): number {
85
- return Hash.random(this)
86
- },
87
80
  toJSON<A>(this: MutableRef<A>) {
88
81
  return {
89
82
  _id: "MutableRef",
package/src/Ref.ts CHANGED
@@ -12,9 +12,7 @@
12
12
  * @since 2.0.0
13
13
  */
14
14
  import * as Effect from "./Effect.ts"
15
- import * as Equal from "./Equal.ts"
16
15
  import { dual, identity } from "./Function.ts"
17
- import * as Hash from "./Hash.ts"
18
16
  import { PipeInspectableProto } from "./internal/core.ts"
19
17
  import * as MutableRef from "./MutableRef.ts"
20
18
  import type * as Option from "./Option.ts"
@@ -112,12 +110,6 @@ const RefProto = {
112
110
  _A: identity
113
111
  },
114
112
  ...PipeInspectableProto,
115
- [Equal.symbol](that: unknown): boolean {
116
- return this === that
117
- },
118
- [Hash.symbol](): number {
119
- return Hash.random(this)
120
- },
121
113
  toJSON(this: Ref<any>) {
122
114
  return {
123
115
  _id: "Ref",
package/src/Schedule.ts CHANGED
@@ -889,7 +889,8 @@ export const cron: {
889
889
  return Cause.done(Duration.zero)
890
890
  }
891
891
  return effect.sync(() => {
892
- const next = Cron.next(cron, now).getTime()
892
+ let next = Cron.next(cron, now).getTime()
893
+ if (Duration.isZero(Duration.millis(next - now))) next = Cron.next(cron, next).getTime()
893
894
  const duration = Duration.millis(next - now)
894
895
  return [duration, duration]
895
896
  })
@@ -1157,7 +1158,7 @@ export const fixed = (interval: Duration.Input): Schedule<number> => {
1157
1158
  }
1158
1159
  const runningBehind = meta.now > (lastRun + window)
1159
1160
  const boundary = window - ((meta.now - start) % window)
1160
- const delay = runningBehind ? 0 : boundary === 0 ? window : boundary
1161
+ const delay = runningBehind ? 0 : Duration.isZero(Duration.millis(boundary)) ? window : boundary
1161
1162
  lastRun = runningBehind ? meta.now : meta.now + delay
1162
1163
  return [meta.attempt - 1, Duration.millis(delay)] as const
1163
1164
  })
@@ -1901,10 +1902,11 @@ export {
1901
1902
  export const windowed = (interval: Duration.Input): Schedule<number> => {
1902
1903
  const window = Duration.toMillis(Duration.fromInputUnsafe(interval))
1903
1904
  return fromStepWithMetadata(effect.succeed((meta) =>
1904
- effect.sync(() => [
1905
- meta.attempt - 1,
1906
- window === 0 ? Duration.zero : Duration.millis(window - (meta.elapsed % window))
1907
- ])
1905
+ effect.sync(() => {
1906
+ if (window === 0) return [meta.attempt - 1, Duration.zero] as const
1907
+ const delay = Duration.millis(window - (meta.elapsed % window))
1908
+ return [meta.attempt - 1, Duration.isZero(delay) ? Duration.millis(window) : delay] as const
1909
+ })
1908
1910
  ))
1909
1911
  }
1910
1912
 
package/src/Stream.ts CHANGED
@@ -15245,12 +15245,13 @@ export const groupAdjacentBy: {
15245
15245
  return loop
15246
15246
  })
15247
15247
  )
15248
- let done = false
15249
- return Pull.catchDone(Effect.suspend(() => done ? Cause.done() : loop), () => {
15250
- done = true
15251
- const out = group
15248
+ let cause: Cause.Cause<E | Cause.Done> | undefined
15249
+ return Effect.catchCause(Effect.suspend(() => cause ? Effect.failCause(cause) : loop), (cause_) => {
15250
+ cause = cause_
15251
+ if (group === undefined) return Effect.failCause(cause_)
15252
+ const out = Arr.of([currentKey, group] as const)
15252
15253
  group = undefined
15253
- return out && Arr.isArrayNonEmpty(out) ? Effect.succeed(Arr.of([currentKey, out])) : Cause.done()
15254
+ return Effect.succeed(out)
15254
15255
  })
15255
15256
  })))
15256
15257
 
@@ -737,23 +737,48 @@ class MiddlewareImpl {
737
737
  }
738
738
  }
739
739
 
740
- // The innermost middleware comes first.
740
+ // The innermost middleware comes first. Each stack keeps its own order, and
741
+ // where that leaves the order open, stacks provided further outside, which come
742
+ // first in the context, run further outside. Stacks that order the same
743
+ // middleware both ways have no common order, so insertion order decides.
741
744
  const getMiddleware = (context: Context.Context<never>): ReadonlyArray<AnyMiddlewareFn> => {
742
- const stacks: Array<ReadonlyArray<AnyMiddlewareFn>> = []
743
- let maxLength = 0
744
- for (const [key, value] of context.mapUnsafe) {
745
- if (key.startsWith(middlewareKeyPrefix)) {
746
- stacks.push(value)
747
- maxLength = Math.max(maxLength, value.length)
745
+ // The middleware directly inside each middleware, and how many of those
746
+ // directly outside it are still unplaced.
747
+ const inners = new Map<AnyMiddlewareFn, Array<AnyMiddlewareFn>>()
748
+ const unplacedOuters = new Map<AnyMiddlewareFn, number>()
749
+ for (const [key, stack] of context.mapUnsafe) {
750
+ if (!key.startsWith(middlewareKeyPrefix)) continue
751
+ for (let i = stack.length - 1; i >= 0; i--) {
752
+ const fn = stack[i]
753
+ if (!inners.has(fn)) {
754
+ inners.set(fn, [])
755
+ unplacedOuters.set(fn, 0)
756
+ }
757
+ const outer = stack[i + 1]
758
+ if (outer !== undefined && outer !== fn) {
759
+ inners.get(outer)!.push(fn)
760
+ unplacedOuters.set(fn, unplacedOuters.get(fn)! + 1)
761
+ }
748
762
  }
749
763
  }
750
- const unique = new Set<AnyMiddlewareFn>()
751
- for (let i = maxLength - 1; i >= 0; i--) {
752
- for (const stack of stacks) {
753
- if (i < stack.length) unique.add(stack[i])
764
+ // Repeatedly places the first middleware with nothing left to place outside it.
765
+ const outermostFirst: Array<AnyMiddlewareFn> = []
766
+ while (unplacedOuters.size > 0) {
767
+ let next = unplacedOuters.keys().next().value!
768
+ for (const [fn, count] of unplacedOuters) {
769
+ if (count === 0) {
770
+ next = fn
771
+ break
772
+ }
773
+ }
774
+ unplacedOuters.delete(next)
775
+ outermostFirst.push(next)
776
+ for (const fn of inners.get(next)!) {
777
+ const count = unplacedOuters.get(fn)
778
+ if (count !== undefined) unplacedOuters.set(fn, count - 1)
754
779
  }
755
780
  }
756
- return Arr.fromIterable(unique).reverse()
781
+ return outermostFirst.reverse()
757
782
  }
758
783
 
759
784
  const isMcpError = (error: unknown): error is McpErrorBase =>
@@ -2099,7 +2124,7 @@ const runWithRuntime = Effect.fnUntraced(function*(
2099
2124
  const selectedProtocol = clientState?.protocol ?? runtime.protocols[0]
2100
2125
  const profile = reverseKey?.profile ?? clientState?.profile ?? {
2101
2126
  protocolVersion: selectedProtocol.protocolVersion,
2102
- clientCapabilities: {},
2127
+ clientCapabilities: new McpSchema.ClientCapabilities({}),
2103
2128
  clientInfo: { name: "unknown", version: "unknown" }
2104
2129
  }
2105
2130
  return RcMap.get(
@@ -3081,40 +3106,40 @@ export const registerToolkit: <Tools extends Record<string, Tool.Any>>(
3081
3106
  const annotations = tool.annotations
3082
3107
  const toolMeta = Context.getOrUndefined(annotations, Tool.Meta)
3083
3108
  const isDeclaredFailure = Schema.is(tool.failureSchema)
3084
- const encodeFailure = Schema.encodeUnknownEffect(tool.failureSchema) as (
3085
- error: unknown
3086
- ) => Effect.Effect<unknown, Schema.SchemaError, Tool.HandlerServices<Tools[keyof Tools]>>
3087
- const declaredFailureResult = (error: unknown) =>
3088
- error instanceof Error && error.message !== ""
3089
- ? Effect.succeed(toolErrorResult(error.message))
3090
- : Effect.map(encodeFailure(error), (encoded) =>
3091
- new CallToolResult({ isError: true, content: toolResultContent(encoded) }))
3092
- // `structuredContent` must be JSON and match the advertised `outputSchema`, which is derived from the JSON codec.
3093
- // The Toolkit has already encoded the result, so only the JSON conversion of its encoded form runs here.
3094
- const encodeStructured = Schema.encodeUnknownEffect(Schema.toCodecJson(Schema.toEncoded(tool.successSchema))) as (
3095
- result: unknown
3096
- ) => Effect.Effect<unknown, Schema.SchemaError, Tool.HandlerServices<Tools[keyof Tools]>>
3097
- const successResult = (result: unknown) =>
3098
- encodeStructured(result).pipe(
3099
- Effect.mapError((cause) =>
3109
+ // MCP content is JSON, so results and declared failures go through their JSON codec.
3110
+ const encodeJson = (schema: Schema.Constraint) => {
3111
+ const encode = Schema.encodeUnknownEffect(Schema.toCodecJson(schema)) as (
3112
+ value: unknown
3113
+ ) => Effect.Effect<unknown, Schema.SchemaError, Tool.HandlerServices<Tools[keyof Tools]>>
3114
+ return (toolResult: unknown) =>
3115
+ Effect.mapError(encode(toolResult), (cause) =>
3100
3116
  AiError.make({
3101
3117
  module: "McpServer",
3102
3118
  method: "registerToolkit",
3103
3119
  reason: new AiError.ToolResultEncodingError({
3104
3120
  toolName: tool.name,
3105
- toolResult: result,
3121
+ toolResult,
3106
3122
  description: cause.message
3107
3123
  })
3108
- })
3109
- ),
3110
- Effect.map((structuredContent) =>
3111
- new CallToolResult({
3112
- isError: false,
3113
- structuredContent,
3114
- content: toolResultContent(structuredContent)
3115
- })
3116
- )
3117
- )
3124
+ }))
3125
+ }
3126
+ const encodeFailure = encodeJson(tool.failureSchema)
3127
+ // Successes and "return"-mode failures arrive already encoded by the Toolkit; only their JSON conversion runs here.
3128
+ const encodeReturnedFailure = encodeJson(Schema.toEncoded(tool.failureSchema))
3129
+ const encodeStructured = encodeJson(Schema.toEncoded(tool.successSchema))
3130
+ const failureResult = (encoded: unknown) =>
3131
+ new CallToolResult({ isError: true, content: toolResultContent(encoded) })
3132
+ const declaredFailureResult = (error: unknown) =>
3133
+ error instanceof Error && error.message !== ""
3134
+ ? Effect.succeed(toolErrorResult(error.message))
3135
+ : Effect.map(encodeFailure(error), failureResult)
3136
+ const successResult = (result: unknown) =>
3137
+ Effect.map(encodeStructured(result), (structuredContent) =>
3138
+ new CallToolResult({
3139
+ isError: false,
3140
+ structuredContent,
3141
+ content: toolResultContent(structuredContent)
3142
+ }))
3118
3143
  const handleCause = (cause: Cause.Cause<unknown>) => {
3119
3144
  const failure = Cause.findFail(cause)
3120
3145
  if (Result.isSuccess(failure)) {
@@ -3183,9 +3208,7 @@ export const registerToolkit: <Tools extends Record<string, Tool.Any>>(
3183
3208
  }
3184
3209
  // Declared failures return their encoded payload; anything else is classified by origin.
3185
3210
  return result.failureOrigin === "handler"
3186
- ? Effect.succeed(
3187
- new CallToolResult({ isError: true, content: toolResultContent(result.encodedResult) })
3188
- )
3211
+ ? Effect.map(encodeReturnedFailure(result.encodedResult), failureResult)
3189
3212
  : Effect.failCause(Cause.annotate(
3190
3213
  Cause.fail(result.result),
3191
3214
  Context.make(Toolkit.FailureOrigin, result.failureOrigin ?? "result")
@@ -203,13 +203,12 @@ const projectContent = Match.type<PublicMcpSchema.ContentBlock>().pipe(
203
203
  Match.exhaustive
204
204
  )
205
205
 
206
- const projectResource = (resource: PublicMcpSchema.Resource) =>
207
- McpProtocol.transcode(PublicMcpSchema.Resource, McpSchema.Resource, resource)
206
+ // Registrations may be plain objects, so encode with the wire struct.
207
+ const projectResource = (resource: PublicMcpSchema.Resource) => Schema.encodeEffect(McpSchema.Resource)(resource)
208
208
  const projectResourceTemplate = (resourceTemplate: PublicMcpSchema.ResourceTemplate) =>
209
- McpProtocol.transcode(PublicMcpSchema.ResourceTemplate, McpSchema.ResourceTemplate, resourceTemplate)
209
+ Schema.encodeEffect(McpSchema.ResourceTemplate)(resourceTemplate)
210
210
 
211
- const projectPrompt = (prompt: PublicMcpSchema.Prompt) =>
212
- McpProtocol.transcode(PublicMcpSchema.Prompt, McpSchema.Prompt, prompt)
211
+ const projectPrompt = (prompt: PublicMcpSchema.Prompt) => Schema.encodeEffect(McpSchema.Prompt)(prompt)
213
212
 
214
213
  const projectTool = (tool: PublicMcpSchema.Tool) => McpProtocol.transcode(PublicMcpSchema.Tool, McpSchema.Tool, tool)
215
214
 
@@ -38,11 +38,17 @@ export function resolveReference($ref: string, definitions: JsonSchema.Definitio
38
38
  export function resolveTopLevelReference(
39
39
  document: JsonSchema.Document<"draft-2020-12">
40
40
  ): JsonSchema.Document<"draft-2020-12"> {
41
- if (typeof document.schema.$ref !== "string") return document
42
- return {
43
- ...document,
44
- schema: resolveReference(document.schema.$ref, document.definitions)
41
+ let schema = document.schema
42
+ if (typeof schema.$ref !== "string") return document
43
+ const visited = new Set<string>()
44
+ while (typeof schema.$ref === "string") {
45
+ if (visited.has(schema.$ref)) {
46
+ throw new Error(`Circular reference ${JSON.stringify(schema.$ref)}`)
47
+ }
48
+ visited.add(schema.$ref)
49
+ schema = resolveReference(schema.$ref, document.definitions)
45
50
  }
51
+ return { ...document, schema }
46
52
  }
47
53
 
48
54
  /**
@@ -267,7 +267,11 @@ export const make = Effect.gen(function*() {
267
267
  id: activityPrimaryKey(options.activity.name, options.attempt)
268
268
  })
269
269
  if (Option.isNone(requestId)) return
270
- yield* sharding.reset(requestId.value)
270
+ // Reset only the Suspended reply this runner saw. If another runner has
271
+ // stored a newer reply, storage skips the reset and the caller reads it.
272
+ const reply = yield* replyForRequestId(requestId.value)
273
+ if (Option.isNone(reply) || !isSuspendedReply(reply.value)) return
274
+ yield* sharding.reset(requestId.value, { expectedReplyId: Snowflake.Snowflake(reply.value.id) })
271
275
  },
272
276
  Effect.retry({
273
277
  times: 3,
@@ -344,7 +348,7 @@ export const make = Effect.gen(function*() {
344
348
  }
345
349
  const reply = yield* replyForRequestId(requestId.value)
346
350
  if (Option.isNone(reply)) return
347
- yield* sharding.reset(requestId.value)
351
+ yield* sharding.reset(requestId.value, { expectedReplyId: Snowflake.Snowflake(reply.value.id) })
348
352
  }, Effect.scoped)
349
353
 
350
354
  // Subscribe to the run's next reply before reading storage, so a reply
@@ -865,7 +869,7 @@ export const make = Effect.gen(function*() {
865
869
  }
866
870
  const workflow = workflows.get(workflowName)
867
871
  if (workflow) {
868
- return yield* Effect.orDie(sendDiscard({
872
+ return yield* retryPersistence(sendDiscard({
869
873
  rpc: DeferredRpc,
870
874
  address: entityAddressFor({
871
875
  workflow,
@@ -879,7 +883,7 @@ export const make = Effect.gen(function*() {
879
883
  }))
880
884
  }
881
885
  const client = yield* RcMap.get(clientsPartial, workflowName)
882
- return yield* Effect.orDie(
886
+ return yield* retryPersistence(
883
887
  client(executionId).deferred({
884
888
  name: deferredName,
885
889
  exit
@@ -890,27 +894,24 @@ export const make = Effect.gen(function*() {
890
894
  ),
891
895
 
892
896
  scheduleClock(workflow, options) {
893
- return DateTime.now.pipe(
894
- Effect.flatMap((now) =>
895
- sendDiscard({
896
- rpc: ClockRpc,
897
- address: entityAddressFor({
898
- workflow,
899
- entityType: ClockEntity.type,
900
- executionId: options.executionId
901
- }),
902
- payload: {
903
- name: options.clock.name,
904
- workflowName: workflow._tag,
905
- wakeUp: DateTime.mapEpochMillis(
906
- DateTime.addDuration(now, options.clock.duration),
907
- Math.ceil
908
- )
909
- }
910
- })
911
- ),
912
- Effect.orDie
913
- )
897
+ // Compute the deadline once, so a retried save keeps the original wake-up.
898
+ return Effect.flatMap(DateTime.now, (now) =>
899
+ retryPersistence(sendDiscard({
900
+ rpc: ClockRpc,
901
+ address: entityAddressFor({
902
+ workflow,
903
+ entityType: ClockEntity.type,
904
+ executionId: options.executionId
905
+ }),
906
+ payload: {
907
+ name: options.clock.name,
908
+ workflowName: workflow._tag,
909
+ wakeUp: DateTime.mapEpochMillis(
910
+ DateTime.addDuration(now, options.clock.duration),
911
+ Math.ceil
912
+ )
913
+ }
914
+ })))
914
915
  }
915
916
  })
916
917
 
@@ -922,9 +923,9 @@ const retryPolicy = Schedule.min([
922
923
  Schedule.spaced("1 minute")
923
924
  ])
924
925
 
925
- // A transient storage failure must not become the workflow result. Requests
926
- // are saved under a primary key, so a retry after a lost acknowledgement is
927
- // deduplicated instead of running the activity again.
926
+ // A transient storage failure must not become the workflow result. Activity,
927
+ // clock and deferred requests are saved under a primary key, so a retry after
928
+ // a lost acknowledgement is deduplicated instead of stored twice.
928
929
  const retryPersistence = <A, E extends { readonly _tag: string }, R>(effect: Effect.Effect<A, E, R>) =>
929
930
  effect.pipe(
930
931
  Effect.retry({ schedule: retryPolicy, while: (e) => e._tag === "PersistenceError" }),
@@ -148,7 +148,20 @@ export interface Entity<
148
148
  readonly concurrency?: number | "unbounded" | undefined
149
149
  readonly mailboxCapacity?: number | "unbounded" | undefined
150
150
  readonly mailboxMetrics?: boolean | undefined
151
+ /**
152
+ * When `true`, a handler defect fails only its request instead of
153
+ * restarting the entity and replaying its in-flight requests.
154
+ */
151
155
  readonly disableFatalDefects?: boolean | undefined
156
+ /**
157
+ * Schedule for restarting an entity after a defect and for retrying failed
158
+ * handler construction.
159
+ *
160
+ * Backoff is tracked per entity and resets once a request completes.
161
+ * When the policy ends, the default (500 ms growing by 1.5x, capped at 10
162
+ * seconds) continues, so restarts never stop; use `disableFatalDefects` to
163
+ * avoid them.
164
+ */
152
165
  readonly defectRetryPolicy?: Schedule.Schedule<any, unknown> | undefined
153
166
  readonly spanAttributes?: Record<string, string> | undefined
154
167
  }
@@ -193,7 +206,20 @@ export interface Entity<
193
206
  readonly maxIdleTime?: Duration.Input | undefined
194
207
  readonly mailboxCapacity?: number | "unbounded" | undefined
195
208
  readonly mailboxMetrics?: boolean | undefined
209
+ /**
210
+ * When `true`, a handler defect fails only its request instead of
211
+ * restarting the entity and replaying its in-flight requests.
212
+ */
196
213
  readonly disableFatalDefects?: boolean | undefined
214
+ /**
215
+ * Schedule for restarting an entity after a defect and for retrying failed
216
+ * handler construction.
217
+ *
218
+ * Backoff is tracked per entity and resets once a request completes.
219
+ * When the policy ends, the default (500 ms growing by 1.5x, capped at 10
220
+ * seconds) continues, so restarts never stop; use `disableFatalDefects` to
221
+ * avoid them.
222
+ */
197
223
  readonly defectRetryPolicy?: Schedule.Schedule<any, unknown> | undefined
198
224
  readonly spanAttributes?: Record<string, string> | undefined
199
225
  }
@@ -290,7 +316,20 @@ const Proto = {
290
316
  readonly concurrency?: number | "unbounded" | undefined
291
317
  readonly mailboxCapacity?: number | "unbounded" | undefined
292
318
  readonly mailboxMetrics?: boolean | undefined
319
+ /**
320
+ * When `true`, a handler defect fails only its request instead of
321
+ * restarting the entity and replaying its in-flight requests.
322
+ */
293
323
  readonly disableFatalDefects?: boolean | undefined
324
+ /**
325
+ * Schedule for restarting an entity after a defect and for retrying failed
326
+ * handler construction.
327
+ *
328
+ * Backoff is tracked per entity and resets once a request completes.
329
+ * When the policy ends, the default (500 ms growing by 1.5x, capped at 10
330
+ * seconds) continues, so restarts never stop; use `disableFatalDefects` to
331
+ * avoid them.
332
+ */
294
333
  readonly defectRetryPolicy?: Schedule.Schedule<any, unknown> | undefined
295
334
  readonly spanAttributes?: Record<string, string> | undefined
296
335
  }
@@ -339,7 +378,20 @@ const Proto = {
339
378
  readonly maxIdleTime?: Duration.Input | undefined
340
379
  readonly mailboxCapacity?: number | "unbounded" | undefined
341
380
  readonly mailboxMetrics?: boolean | undefined
381
+ /**
382
+ * When `true`, a handler defect fails only its request instead of
383
+ * restarting the entity and replaying its in-flight requests.
384
+ */
342
385
  readonly disableFatalDefects?: boolean | undefined
386
+ /**
387
+ * Schedule for restarting an entity after a defect and for retrying failed
388
+ * handler construction.
389
+ *
390
+ * Backoff is tracked per entity and resets once a request completes.
391
+ * When the policy ends, the default (500 ms growing by 1.5x, capped at 10
392
+ * seconds) continues, so restarts never stop; use `disableFatalDefects` to
393
+ * avoid them.
394
+ */
343
395
  readonly defectRetryPolicy?: Schedule.Schedule<any, unknown> | undefined
344
396
  readonly spanAttributes?: Record<string, string> | undefined
345
397
  }
@@ -30,6 +30,7 @@ import * as DeliverAt from "./DeliverAt.ts"
30
30
  import type { EntityAddress } from "./EntityAddress.ts"
31
31
  import * as Envelope from "./Envelope.ts"
32
32
  import * as ClusterAbandon from "./internal/clusterAbandon.ts"
33
+ import { TransactionNotifications } from "./internal/transactionNotifications.ts"
33
34
  import * as Message from "./Message.ts"
34
35
  import * as Reply from "./Reply.ts"
35
36
  import * as ShardId from "./ShardId.ts"
@@ -223,8 +224,8 @@ export class MessageStorage extends Context.Service<MessageStorage, {
223
224
  /**
224
225
  * Wraps requests in storage transactions.
225
226
  *
226
- * `WithExit` replies saved in a transaction notify callers after the outermost
227
- * commit; stream chunks notify immediately. Replies saved in a failed
227
+ * `WithExit` replies and discarded sends saved in a transaction notify after
228
+ * the outermost commit; stream chunks notify immediately. Replies saved in a failed
228
229
  * transaction must be discarded. The wrapper may fail only with the wrapped
229
230
  * effect's own failure or a defect.
230
231
  */
@@ -532,10 +533,6 @@ export type EncodedRepliesOptions<A> = {
532
533
  readonly cursor: Option.Option<A>
533
534
  }
534
535
 
535
- const TransactionNotifications = Context.Reference<
536
- Array<Effect.Effect<void, PersistenceError | MalformedMessage>> | undefined
537
- >("effect/cluster/MessageStorage/TransactionNotifications", { defaultValue: () => undefined })
538
-
539
536
  /**
540
537
  * Wraps a concrete message storage implementation with reply-handler management.
541
538
  *
@@ -32,6 +32,7 @@ import type { MalformedMessage, PersistenceError } from "./ClusterError.ts"
32
32
  import { AlreadyProcessingMessage, EntityNotAssignedToRunner, MailboxFull, RunnerUnavailable } from "./ClusterError.ts"
33
33
  import { Persisted } from "./ClusterSchema.ts"
34
34
  import * as Envelope from "./Envelope.ts"
35
+ import { TransactionNotifications } from "./internal/transactionNotifications.ts"
35
36
  import * as Message from "./Message.ts"
36
37
  import * as MessageStorage from "./MessageStorage.ts"
37
38
  import * as Reply from "./Reply.ts"
@@ -180,6 +181,19 @@ export const make: (
180
181
 
181
182
  const requestIdRewrites = new Map<Snowflake.Snowflake, Snowflake.Snowflake>()
182
183
 
184
+ // A message saved in a transaction is invisible to readers until the commit,
185
+ // so its wake-up waits for the commit. A failure then can no longer be
186
+ // returned and is dropped; the next storage poll delivers the message.
187
+ const notifyAfterCommit = <E>(message: Message.Outgoing<any>, notify: Effect.Effect<void, E>) => {
188
+ const rpc = message.rpc as any as Rpc.AnyWithProps
189
+ if (!Context.get(rpc.annotations, Persisted)) return notify
190
+ return TransactionNotifications.use((notifications) =>
191
+ notifications === undefined ? notify : Effect.sync(() => {
192
+ notifications.push(Effect.ignore(notify))
193
+ })
194
+ )
195
+ }
196
+
183
197
  function notifyWith<E>(
184
198
  message: Message.Outgoing<any>,
185
199
  afterPersist: (message: Message.Outgoing<any>, isDuplicate: boolean) => Effect.Effect<void, E>
@@ -471,7 +485,7 @@ export const make: (
471
485
  const { discard, message } = options_
472
486
  return notifyWith(message, (message, duplicate) => {
473
487
  if (discard || message._tag === "OutgoingEnvelope") {
474
- return options.notify(options_)
488
+ return notifyAfterCommit(message, options.notify(options_))
475
489
  } else if (!duplicate && Option.isSome(options_.address)) {
476
490
  return Effect.catch(
477
491
  options.send({
@@ -499,7 +513,7 @@ export const make: (
499
513
  }
500
514
  return notifyWith(message, (message) => {
501
515
  if (options.discard || message._tag === "OutgoingEnvelope") {
502
- return notifyEntity(message)
516
+ return notifyAfterCommit(message, notifyEntity(message))
503
517
  }
504
518
  return Effect.andThen(
505
519
  notifyEntity(message),