@typeonce/effect-machine 0.17.0 → 0.19.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 (45) hide show
  1. package/README.md +133 -80
  2. package/dist/Machine.d.ts +434 -297
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +13 -55
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/cluster.d.ts +2 -2
  7. package/dist/internal/machine/cluster.d.ts.map +1 -1
  8. package/dist/internal/machine/cluster.js +2 -1
  9. package/dist/internal/machine/cluster.js.map +1 -1
  10. package/dist/internal/machine/invocation.d.ts.map +1 -1
  11. package/dist/internal/machine/invocation.js +1 -1
  12. package/dist/internal/machine/invocation.js.map +1 -1
  13. package/dist/internal/machine/machine.d.ts.map +1 -1
  14. package/dist/internal/machine/machine.js +48 -10
  15. package/dist/internal/machine/machine.js.map +1 -1
  16. package/dist/internal/machine/serialization.d.ts.map +1 -1
  17. package/dist/internal/machine/serialization.js +75 -18
  18. package/dist/internal/machine/serialization.js.map +1 -1
  19. package/dist/internal/testing/machine/verification.d.ts +1 -1
  20. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  21. package/dist/internal/testing/machine/verification.js +9 -6
  22. package/dist/internal/testing/machine/verification.js.map +1 -1
  23. package/dist/testing/MachineTest.d.ts +13 -10
  24. package/dist/testing/MachineTest.d.ts.map +1 -1
  25. package/dist/testing/MachineTest.js +5 -3
  26. package/dist/testing/MachineTest.js.map +1 -1
  27. package/dist/unstable/cluster/ClusterMachine.d.ts +22 -2
  28. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  29. package/dist/unstable/cluster/ClusterMachine.js +4 -0
  30. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  31. package/dist/unstable/reactivity/AtomMachine.d.ts +1 -1
  32. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  33. package/docs/agent-guide.md +189 -83
  34. package/package.json +4 -4
  35. package/src/Machine.ts +845 -1051
  36. package/src/internal/machine/cluster.ts +4 -1
  37. package/src/internal/machine/invocation.ts +1 -1
  38. package/src/internal/machine/machine.ts +64 -8
  39. package/src/internal/machine/serialization.ts +100 -25
  40. package/src/internal/testing/machine/exploration.ts +1 -1
  41. package/src/internal/testing/machine/trace.ts +1 -1
  42. package/src/internal/testing/machine/verification.ts +16 -11
  43. package/src/testing/MachineTest.ts +13 -10
  44. package/src/unstable/cluster/ClusterMachine.ts +51 -1
  45. package/src/unstable/reactivity/AtomMachine.ts +1 -1
@@ -72,6 +72,7 @@ export const RejectionReason = Schema.Literals([
72
72
  "InvalidCheckpoint",
73
73
  "UnsupportedProcessLocal",
74
74
  "TransitionFailure",
75
+ "SnapshotEncodeFailure",
75
76
  "PersistenceFailure",
76
77
  "EmissionFailure"
77
78
  ])
@@ -341,7 +342,9 @@ export const make = <
341
342
  emitted.push(...planned.emittedEvents as any)
342
343
  }
343
344
 
344
- const encoded = yield* internalMachine.encodeSnapshot(rootMachine, current)
345
+ const encoded = yield* internalMachine.encodeSnapshot(rootMachine, current).pipe(
346
+ Effect.mapError((error) => reject("SnapshotEncodeFailure", String(error.cause)))
347
+ )
345
348
  if (emitted.length > 0 && layerOptions?.enqueue === undefined) {
346
349
  return yield* fail("EmissionFailure", "No durable enqueue handler was configured")
347
350
  }
@@ -31,7 +31,7 @@ export interface AnyConfig {
31
31
  export const makeKey = (path: string, id: string): string => `${path.length}:${path}${id}`
32
32
 
33
33
  /** @internal */
34
- export const makeChildId = (path: string, id: string): string => `Machine.invoke:${makeKey(path, id)}`
34
+ export const makeChildId = (path: string, id: string): string => `Machine.invocation:${makeKey(path, id)}`
35
35
 
36
36
  const oneShot = (effect: Effect.Effect<any, any, any>): Logic<void, never, any, any, any> => ({
37
37
  initial: () => Effect.void,
@@ -54,6 +54,7 @@ const TypeId = "~effect/Machine"
54
54
  const ParentTypeId = "~effect/Machine/Parent"
55
55
  export const InvokeTypeId: unique symbol = Symbol.for("effect/Machine/Invoke")
56
56
  export const TransitionTypeId: unique symbol = Symbol.for("effect/Machine/Transition")
57
+ const InvokeBuilderDescriptorTypeId: unique symbol = Symbol("effect/Machine/InvokeBuilderDescriptor")
57
58
  const ChildMachineTypeId = "~effect/Machine/ChildMachine"
58
59
  type IsAny<A> = 0 extends 1 & A ? true : false
59
60
  type MachineRuntimeRequirement = internalRuntime.MachineRuntime
@@ -788,20 +789,75 @@ const captureEventHandlers = (
788
789
  return captured
789
790
  }
790
791
 
792
+ interface InvokeBuilderDescriptor {
793
+ readonly [InvokeBuilderDescriptorTypeId]: typeof InvokeBuilderDescriptorTypeId
794
+ readonly config: Readonly<Record<PropertyKey, unknown>>
795
+ }
796
+
797
+ type InvokeBuilderChannel = "onDone" | "onFailure" | "onElement" | "onSnapshot"
798
+
799
+ const makeInvokeBuilder = (
800
+ config: Readonly<Record<PropertyKey, unknown>>,
801
+ channels: ReadonlyArray<InvokeBuilderChannel>
802
+ ): InvokeBuilderDescriptor => {
803
+ const builder: Record<PropertyKey, unknown> = {
804
+ [InvokeBuilderDescriptorTypeId]: InvokeBuilderDescriptorTypeId as typeof InvokeBuilderDescriptorTypeId,
805
+ config
806
+ }
807
+ for (const channel of channels) {
808
+ if (!hasProperty(config, channel)) {
809
+ builder[channel] = (handler: unknown) => makeInvokeBuilder({ ...config, [channel]: handler }, channels)
810
+ }
811
+ }
812
+ return Object.freeze(builder) as unknown as InvokeBuilderDescriptor
813
+ }
814
+
815
+ const invokeSelector = Object.freeze({
816
+ effect: (id: string, effect: unknown) => makeInvokeBuilder({ id, effect }, ["onDone", "onFailure"]),
817
+ stream: (id: string, stream: unknown) => makeInvokeBuilder({ id, stream }, ["onElement", "onDone", "onFailure"]),
818
+ timer: (id: string, after: unknown) => makeInvokeBuilder({ id, after }, ["onDone"]),
819
+ logic: (id: string, options: Readonly<Record<PropertyKey, unknown>>) =>
820
+ makeInvokeBuilder({ id, ...options }, ["onSnapshot", "onDone", "onFailure"]),
821
+ child: (child: unknown, options?: Readonly<Record<PropertyKey, unknown>>) =>
822
+ makeInvokeBuilder(options === undefined ? { child } : { child, ...options }, [
823
+ "onSnapshot",
824
+ "onDone",
825
+ "onFailure"
826
+ ])
827
+ })
828
+
829
+ const invokeBuilderConfig = (value: unknown, path: string): Readonly<Record<PropertyKey, unknown>> => {
830
+ if (
831
+ typeof value !== "object" || value === null ||
832
+ !hasProperty(value, InvokeBuilderDescriptorTypeId) ||
833
+ value[InvokeBuilderDescriptorTypeId] !== InvokeBuilderDescriptorTypeId
834
+ ) {
835
+ throw new Error(`Machine invocation for state "${path}" must be constructed from its source selector`)
836
+ }
837
+ return (value as unknown as InvokeBuilderDescriptor).config
838
+ }
839
+
791
840
  const captureInvokeDefinition = (
792
841
  invoke: unknown,
793
842
  stateNodes: Machine.StateNodes,
794
843
  path: string
795
844
  ): unknown => {
796
- if (Array.isArray(invoke)) return invoke.map((item) => captureInvokeDefinition(item, stateNodes, path))
797
- if (typeof invoke !== "object" || invoke === null) return invoke
798
- const captured = { ...(invoke as Record<PropertyKey, unknown>) }
799
- for (const key of ["onElement", "onDone", "onFailure", "onSnapshot"] as const) {
800
- if (captured[key] !== undefined) {
801
- captured[key] = captureTransition(captured[key], stateNodes, path, key)
845
+ if (typeof invoke !== "function") {
846
+ throw new Error(`Machine invocation for state "${path}" must be a source-first callback`)
847
+ }
848
+ const authored = invoke(invokeSelector)
849
+ const definitions = Array.isArray(authored) ? authored : [authored]
850
+ const capturedDefinitions = definitions.map((definition) => {
851
+ const captured = { ...invokeBuilderConfig(definition, path) }
852
+ for (const key of ["onElement", "onDone", "onFailure", "onSnapshot"] as const) {
853
+ if (captured[key] !== undefined) {
854
+ captured[key] = captureTransition(captured[key], stateNodes, path, key)
855
+ }
802
856
  }
803
- }
804
- return captured
857
+ return captured
858
+ })
859
+ if (Array.isArray(authored)) return capturedDefinitions
860
+ return capturedDefinitions[0]
805
861
  }
806
862
 
807
863
  const flattenHandlers = (
@@ -31,22 +31,48 @@ const EncodedSnapshotSchema = Schema.Struct({
31
31
  _tag: Schema.Literal("MachineSnapshot"),
32
32
  active: Schema.Array(Schema.Struct({
33
33
  path: Schema.String,
34
- value: Schema.optional(Schema.Unknown)
34
+ value: Schema.optionalKey(Schema.Json)
35
35
  })),
36
- completed: Schema.optional(Schema.Array(Schema.Struct({
36
+ completed: Schema.optionalKey(Schema.Array(Schema.Struct({
37
37
  path: Schema.String,
38
- output: Schema.optional(Schema.Unknown)
38
+ output: Schema.optionalKey(Schema.Json)
39
39
  }))),
40
- history: Schema.optional(Schema.Record(
40
+ history: Schema.optionalKey(Schema.Record(
41
41
  Schema.String,
42
42
  Schema.Struct({
43
43
  mode: Schema.Literals(["shallow", "deep"]),
44
44
  active: Schema.Array(Schema.String),
45
- values: Schema.Record(Schema.String, Schema.Unknown)
45
+ values: Schema.Record(Schema.String, Schema.Json)
46
46
  })
47
47
  ))
48
48
  })
49
49
 
50
+ const jsonCodecCache = new WeakMap<object, Schema.Top>()
51
+
52
+ const getJsonCodec = (schema: Schema.Top): Schema.Top => {
53
+ const key = schema as object
54
+ const cached = jsonCodecCache.get(key)
55
+ if (cached !== undefined) return cached
56
+ const codec = Schema.toCodecJson(schema)
57
+ jsonCodecCache.set(key, codec)
58
+ return codec
59
+ }
60
+
61
+ const encodeError = (
62
+ machine: Machine.Any,
63
+ options: {
64
+ readonly boundary: "state" | "output" | "history"
65
+ readonly state: string
66
+ },
67
+ cause: Schema.SchemaError | Cause.Cause<unknown>
68
+ ): MachineSchemaEncodeError =>
69
+ new MachineSchemaEncodeError({
70
+ machineId: machine.id,
71
+ boundary: options.boundary,
72
+ state: options.state,
73
+ cause
74
+ })
75
+
50
76
  const encodeBoundary = (
51
77
  machine: Machine.Any,
52
78
  schema: Schema.Top,
@@ -55,16 +81,14 @@ const encodeBoundary = (
55
81
  readonly boundary: "state" | "output" | "history"
56
82
  readonly state: string
57
83
  }
58
- ): Effect.Effect<unknown, MachineSchemaEncodeError, unknown> =>
59
- Schema.encodeUnknownEffect(schema)(value).pipe(
60
- Effect.mapError((cause) =>
61
- new MachineSchemaEncodeError({
62
- machineId: machine.id,
63
- boundary: options.boundary,
64
- state: options.state,
65
- cause
66
- })
67
- )
84
+ ): Effect.Effect<Schema.Json, MachineSchemaEncodeError, unknown> =>
85
+ Effect.try({
86
+ try: () => getJsonCodec(schema),
87
+ catch: (cause) => encodeError(machine, options, Cause.die(cause))
88
+ }).pipe(
89
+ Effect.flatMap((codec) => Schema.encodeUnknownEffect(codec)(value)),
90
+ Effect.flatMap(Schema.decodeUnknownEffect(Schema.Json)),
91
+ Effect.mapError((cause) => cause instanceof MachineSchemaEncodeError ? cause : encodeError(machine, options, cause))
68
92
  )
69
93
 
70
94
  const decodeEncodedBoundary = (
@@ -76,14 +100,26 @@ const decodeEncodedBoundary = (
76
100
  readonly state: string
77
101
  }
78
102
  ): Effect.Effect<unknown, MachineSchemaDecodeError, unknown> =>
79
- Schema.decodeUnknownEffect(schema)(value).pipe(
80
- Effect.mapError((cause) =>
103
+ Effect.try({
104
+ try: () => getJsonCodec(schema),
105
+ catch: (cause) =>
81
106
  new MachineSchemaDecodeError({
82
107
  machineId: machine.id,
83
108
  boundary: options.boundary,
84
109
  state: options.state,
85
- cause
110
+ cause: Cause.die(cause)
86
111
  })
112
+ }).pipe(
113
+ Effect.flatMap((codec) => Schema.decodeUnknownEffect(codec)(value)),
114
+ Effect.mapError((cause) =>
115
+ cause instanceof MachineSchemaDecodeError ?
116
+ cause :
117
+ new MachineSchemaDecodeError({
118
+ machineId: machine.id,
119
+ boundary: options.boundary,
120
+ state: options.state,
121
+ cause
122
+ })
87
123
  )
88
124
  )
89
125
 
@@ -91,7 +127,7 @@ const getCompletionSchema = (
91
127
  machine: Machine.Any,
92
128
  configuration: ActiveConfiguration,
93
129
  path: string
94
- ): Schema.Top => {
130
+ ): Schema.Top | undefined => {
95
131
  const node = getNode(machine, path)
96
132
  if (node.type === "compound") {
97
133
  const child = getActiveChildPath(machine, configuration, path)
@@ -100,7 +136,7 @@ const getCompletionSchema = (
100
136
  }
101
137
  return getCompletionSchema(machine, configuration, child)
102
138
  }
103
- return node.output ?? Schema.Void
139
+ return node.output
104
140
  }
105
141
 
106
142
  /** Defensively validates and normalizes an in-memory logical snapshot. Unlike
@@ -132,9 +168,17 @@ export const normalizeSnapshotEffect = <const States extends Machine.StateSchema
132
168
  throw new Error(`Machine snapshot contains invalid completion "${path}"`)
133
169
  }
134
170
  completionPaths.add(path)
171
+ const schema = getCompletionSchema(machine, configuration, path)
172
+ if (schema === undefined) {
173
+ if (completion.output !== undefined) {
174
+ throw new Error(`Machine snapshot contains an output for state "${path}" without an output schema`)
175
+ }
176
+ outputs.set(path, undefined)
177
+ continue
178
+ }
135
179
  outputs.set(
136
180
  path,
137
- yield* decodeBoundary(machine, getCompletionSchema(machine, configuration, path), completion.output, {
181
+ yield* decodeBoundary(machine, schema, completion.output, {
138
182
  boundary: "output",
139
183
  state: path
140
184
  })
@@ -242,9 +286,17 @@ export const encodeSnapshot = (
242
286
  if (!configuration.active.has(path) || !isActiveFinalNode(machine, configuration, path)) {
243
287
  throw new Error(`Machine encoded snapshot contains invalid completion "${path}"`)
244
288
  }
289
+ const schema = getCompletionSchema(machine, configuration, path)
290
+ if (schema === undefined) {
291
+ if (output !== undefined) {
292
+ throw new Error(`Machine snapshot contains an output for state "${path}" without an output schema`)
293
+ }
294
+ completed.push({ path })
295
+ continue
296
+ }
245
297
  const encodedOutput = yield* encodeBoundary(
246
298
  machine,
247
- getCompletionSchema(machine, configuration, path),
299
+ schema,
248
300
  output,
249
301
  {
250
302
  boundary: "output",
@@ -289,7 +341,7 @@ export const encodeSnapshot = (
289
341
  })
290
342
  )
291
343
  }
292
- const encodedValues: Record<string, unknown> = {}
344
+ const encodedValues: Record<string, Schema.Json> = {}
293
345
  for (const path of record.active) {
294
346
  const stateNode = machine.stateNodes.byPath.get(path)
295
347
  if (
@@ -338,12 +390,21 @@ export const encodeSnapshot = (
338
390
  }
339
391
  }
340
392
 
341
- return {
393
+ const encoded: Machine.EncodedSnapshot = {
342
394
  _tag: "MachineSnapshot" as const,
343
395
  active,
344
396
  ...(completed.length === 0 ? {} : { completed }),
345
397
  ...(Object.keys(history).length === 0 ? {} : { history })
346
398
  }
399
+ return yield* Schema.decodeUnknownEffect(EncodedSnapshotSchema)(encoded).pipe(
400
+ Effect.mapError((cause) =>
401
+ new MachineSchemaEncodeError({
402
+ machineId: machine.id,
403
+ boundary: "configuration",
404
+ cause
405
+ })
406
+ )
407
+ )
347
408
  }).pipe(Effect.catchCause((cause) => failEncodeCause(machine, cause)))
348
409
 
349
410
  export const decodeSnapshot = (
@@ -501,11 +562,25 @@ export const decodeSnapshot = (
501
562
  throw new Error(`Machine encoded snapshot contains invalid completion "${completion.path}"`)
502
563
  }
503
564
  completionPaths.add(completion.path)
565
+ const schema = getCompletionSchema(machine, configuration, completion.path)
566
+ const hasOutput = Object.prototype.hasOwnProperty.call(completion, "output")
567
+ if (schema === undefined) {
568
+ if (hasOutput) {
569
+ throw new Error(
570
+ `Machine encoded snapshot contains an output for state "${completion.path}" without an output schema`
571
+ )
572
+ }
573
+ completions.push({ path: completion.path, output: undefined })
574
+ continue
575
+ }
576
+ if (!hasOutput) {
577
+ throw new Error(`Machine encoded snapshot omits output for state "${completion.path}"`)
578
+ }
504
579
  completions.push({
505
580
  path: completion.path,
506
581
  output: yield* decodeEncodedBoundary(
507
582
  machine,
508
- getCompletionSchema(machine, configuration, completion.path),
583
+ schema,
509
584
  completion.output,
510
585
  {
511
586
  boundary: "output",
@@ -34,7 +34,7 @@ import { makeTransitionCoverageCollector } from "./transitionCoverage.js"
34
34
 
35
35
  type AnyMachine = Machine.Machine.Any
36
36
 
37
- type InputValue<M extends AnyMachine> = Machine.Machine.Input<M>["Type"]
37
+ type InputValue<M extends AnyMachine> = Machine.Machine.Input<M>
38
38
 
39
39
  type ReadyMachine<M extends AnyMachine> =
40
40
  & M
@@ -22,7 +22,7 @@ import type { EnsureExecutable } from "../../machine/readiness.js"
22
22
 
23
23
  type AnyMachine = Machine.Machine.Any
24
24
 
25
- type InputValue<M extends AnyMachine> = Machine.Machine.Input<M>["Type"]
25
+ type InputValue<M extends AnyMachine> = Machine.Machine.Input<M>
26
26
 
27
27
  type StatePath<M extends AnyMachine> = Machine.Machine.StateIdentifier<Machine.Machine.States<M>>
28
28
 
@@ -152,7 +152,7 @@ export const interpretModel = ReferenceModel.interpretModel
152
152
 
153
153
  type AnyMachine = Machine.Machine.Any
154
154
 
155
- type InputValue<M extends AnyMachine> = Machine.Machine.Input<M>["Type"]
155
+ type InputValue<M extends AnyMachine> = Machine.Machine.Input<M>
156
156
 
157
157
  type StatePath<M extends AnyMachine> = Machine.Machine.StateIdentifier<Machine.Machine.States<M>>
158
158
 
@@ -772,7 +772,7 @@ export const observedGraph: <M extends AnyMachine>(
772
772
  traceOrTraces: Trace<M> | ReadonlyArray<Trace<M>>
773
773
  ) => Effect.Effect<
774
774
  ObservedGraph<M>,
775
- Machine.MachineSchemaEncodeError,
775
+ never,
776
776
  Machine.Machine.SnapshotEncodingServices<Machine.Machine.States<M>>
777
777
  > = Effect.fnUntraced(function*<M extends AnyMachine>(
778
778
  machine: M,
@@ -837,19 +837,24 @@ export const observedGraph: <M extends AnyMachine>(
837
837
  }
838
838
  })
839
839
 
840
- const encoded = yield* Effect.forEach(
840
+ const representations = yield* Effect.forEach(
841
841
  occurrences,
842
842
  ({ snapshot }) =>
843
- (Machine.encodeSnapshot as any)(machine, snapshot) as Effect.Effect<
843
+ ((Machine.encodeSnapshot as any)(machine, snapshot) as Effect.Effect<
844
844
  Machine.Machine.EncodedSnapshot,
845
845
  Machine.MachineSchemaEncodeError,
846
846
  Machine.Machine.SnapshotEncodingServices<Machine.Machine.States<M>>
847
- >
847
+ >).pipe(
848
+ Effect.match({
849
+ onFailure: () => ({ identity: snapshot, encoded: undefined }),
850
+ onSuccess: (encoded) => ({ identity: encoded, encoded })
851
+ })
852
+ )
848
853
  )
849
- const encodedIdentity = makeStructuralIdentityIndex()
850
- const occurrenceIds = encoded.map(encodedIdentity)
854
+ const representationIdentity = makeStructuralIdentityIndex()
855
+ const occurrenceIds = representations.map(({ identity }) => representationIdentity(identity))
851
856
  const grouped = new Map<string, {
852
- readonly encoded: Machine.Machine.EncodedSnapshot
857
+ readonly encoded: Machine.Machine.EncodedSnapshot | undefined
853
858
  readonly snapshot: Machine.Machine.Snapshot<Machine.Machine.States<M>>
854
859
  startup: number
855
860
  event: number
@@ -862,7 +867,7 @@ export const observedGraph: <M extends AnyMachine>(
862
867
  grouped.set(
863
868
  id,
864
869
  group = {
865
- encoded: encoded[index]!,
870
+ encoded: representations[index]!.encoded,
866
871
  snapshot: occurrence.snapshot,
867
872
  startup: 0,
868
873
  event: 0,
@@ -879,8 +884,8 @@ export const observedGraph: <M extends AnyMachine>(
879
884
  const node = Graph.addNode(mutable, {
880
885
  id,
881
886
  snapshot: group.snapshot,
882
- encoded: group.encoded,
883
- configuration: group.encoded.active.map(({ path }) => path) as unknown as ReadonlyArray<StatePath<M>>,
887
+ ...(group.encoded === undefined ? {} : { encoded: group.encoded }),
888
+ configuration: rawConfigurationPaths(machine, group.snapshot) as ReadonlyArray<StatePath<M>>,
884
889
  observations: {
885
890
  total: group.startup + group.event + group.microstep,
886
891
  startup: group.startup,
@@ -122,7 +122,7 @@ export const interpretModel: (model: FiniteModel, events: ReadonlyArray<string>)
122
122
 
123
123
  type AnyMachine = Machine.Machine.Any
124
124
 
125
- type InputValue<M extends AnyMachine> = Machine.Machine.Input<M>["Type"]
125
+ type InputValue<M extends AnyMachine> = Machine.Machine.Input<M>
126
126
 
127
127
  type StatePath<M extends AnyMachine> = Machine.Machine.StateIdentifier<Machine.Machine.States<M>>
128
128
 
@@ -151,7 +151,7 @@ type RootReadyMachine<M extends AnyMachine> =
151
151
  * @category models
152
152
  * @since 0.4.0
153
153
  */
154
- export type Scenario<M extends AnyMachine> = Machine.Machine.Input<M> extends typeof Schema.Void ? {
154
+ export type Scenario<M extends AnyMachine> = Machine.Machine.InputSchema<M> extends typeof Schema.Void ? {
155
155
  readonly events: ReadonlyArray<Machine.Machine.InputEvent<M>>
156
156
  }
157
157
  : {
@@ -174,7 +174,7 @@ export type ScenarioOptions<M extends AnyMachine> =
174
174
  readonly maxEvents?: number
175
175
  readonly eventsArbitrary?: FastCheck.Arbitrary<ReadonlyArray<Machine.Machine.InputEvent<M>>>
176
176
  }
177
- & (Machine.Machine.Input<M> extends typeof Schema.Void ? {
177
+ & (Machine.Machine.InputSchema<M> extends typeof Schema.Void ? {
178
178
  readonly inputArbitrary?: never
179
179
  }
180
180
  : {
@@ -1410,7 +1410,7 @@ interface ExploreOptionsBase<M extends AnyMachine, Key extends ExplorationKey> {
1410
1410
  */
1411
1411
  export type ExploreOptions<M extends AnyMachine, Key extends ExplorationKey = ExplorationKey> =
1412
1412
  & ExploreOptionsBase<M, Key>
1413
- & (Machine.Machine.Input<M> extends typeof Schema.Void ? {
1413
+ & (Machine.Machine.InputSchema<M> extends typeof Schema.Void ? {
1414
1414
  readonly input?: never
1415
1415
  }
1416
1416
  : {
@@ -1935,7 +1935,7 @@ export interface ObservedGraphNodeObservations {
1935
1935
  }
1936
1936
 
1937
1937
  /**
1938
- * One full encoded logical snapshot stored in the observed Effect graph.
1938
+ * One decoded logical snapshot stored in the observed Effect graph.
1939
1939
  *
1940
1940
  * @category models
1941
1941
  * @since 0.4.0
@@ -1943,7 +1943,8 @@ export interface ObservedGraphNodeObservations {
1943
1943
  export interface ObservedGraphNode<M extends AnyMachine> {
1944
1944
  readonly id: string
1945
1945
  readonly snapshot: Machine.Machine.Snapshot<Machine.Machine.States<M>>
1946
- readonly encoded: Machine.Machine.EncodedSnapshot
1946
+ /** Canonical JSON representation when this local snapshot is portable. */
1947
+ readonly encoded?: Machine.Machine.EncodedSnapshot
1947
1948
  readonly configuration: ReadonlyArray<StatePath<M>>
1948
1949
  readonly observations: ObservedGraphNodeObservations
1949
1950
  }
@@ -2004,9 +2005,11 @@ export interface ObservedGraph<M extends AnyMachine> {
2004
2005
 
2005
2006
  /**
2006
2007
  * Converts concrete planner traces into an observed logical-state graph.
2007
- * Nodes are deduplicated by the public snapshot encoding and every edge is a
2008
- * concrete startup or public-event macrostep. This intentionally does not
2009
- * claim to be a static or exhaustive graph of the machine.
2008
+ * Portable nodes are deduplicated by the public snapshot encoding. If encoding
2009
+ * fails, process-local values are instead compared by cycle-safe structural
2010
+ * identity and `ObservedGraphNode.encoded` is omitted. Every edge is a concrete
2011
+ * startup or public-event macrostep. This intentionally does not claim to be a
2012
+ * static or exhaustive graph of the machine.
2010
2013
  *
2011
2014
  * @category verification
2012
2015
  * @since 0.4.0
@@ -2016,7 +2019,7 @@ export const observedGraph: <M extends AnyMachine>(
2016
2019
  traceOrTraces: Trace<M> | ReadonlyArray<Trace<M>>
2017
2020
  ) => Effect.Effect<
2018
2021
  ObservedGraph<M>,
2019
- Machine.MachineSchemaEncodeError,
2022
+ never,
2020
2023
  Machine.Machine.SnapshotEncodingServices<Machine.Machine.States<M>>
2021
2024
  > = internal.observedGraph
2022
2025
 
@@ -153,6 +153,7 @@ export const RejectionReason = Schema.Literals([
153
153
  "InvalidCheckpoint",
154
154
  "UnsupportedProcessLocal",
155
155
  "TransitionFailure",
156
+ "SnapshotEncodeFailure",
156
157
  "PersistenceFailure",
157
158
  "EmissionFailure"
158
159
  ])
@@ -246,6 +247,51 @@ type MachineServices<M extends Machine.Machine.Any> =
246
247
 
247
248
  type IsAny<A> = 0 extends (1 & A) ? true : false
248
249
 
250
+ type IsNever<A> = [A] extends [never] ? true : false
251
+
252
+ type IsUnknown<A> = IsAny<A> extends true ? false : unknown extends A ? true : false
253
+
254
+ type IsJsonEncoded<S extends Schema.Top> = IsAny<S["Encoded"]> extends true ? false
255
+ : IsNever<S["Encoded"]> extends true ? false
256
+ : IsUnknown<S["Encoded"]> extends true ? false
257
+ : [S["Encoded"]] extends [Schema.Json] ? true
258
+ : false
259
+
260
+ type NonJsonState<States extends Machine.Machine.StateSchemas> = Machine.Machine.ValuedStateIdentifier<States> extends
261
+ infer StateId
262
+ ? StateId extends Machine.Machine.ValuedStateIdentifier<States> ?
263
+ IsJsonEncoded<Machine.Machine.SchemaByIdentifier<States, StateId>> extends true ? never : StateId
264
+ : never
265
+ : never
266
+
267
+ type NonJsonOutput<States extends Machine.Machine.StateSchemas> = Machine.Machine.DeclaredOutputState<States> extends
268
+ infer StateId
269
+ ? StateId extends Machine.Machine.DeclaredOutputState<States> ?
270
+ Machine.Machine.NodeByIdentifier<States, StateId> extends {
271
+ readonly output: infer Output extends Schema.Top
272
+ } ? IsJsonEncoded<Output> extends true ? never : StateId
273
+ : never
274
+ : never
275
+ : never
276
+
277
+ type NonJsonInputEvent<Events extends ReadonlyArray<Machine.Machine.TaggedSchema>> = {
278
+ readonly [Index in keyof Events]: Events[Index] extends infer EventSchema extends Machine.Machine.TaggedSchema
279
+ ? IsJsonEncoded<EventSchema> extends true ? never : Machine.Machine.TagOf<EventSchema>
280
+ : never
281
+ }[number]
282
+
283
+ type EnsureJsonEncoded<
284
+ States extends Machine.Machine.StateSchemas,
285
+ InputEvents extends ReadonlyArray<Machine.Machine.TaggedSchema>
286
+ > = [NonJsonState<States> | NonJsonOutput<States> | NonJsonInputEvent<InputEvents>] extends [never] ? unknown
287
+ : {
288
+ readonly "~effect/ClusterMachine/NonJsonEncoded": {
289
+ readonly states: NonJsonState<States>
290
+ readonly outputs: NonJsonOutput<States>
291
+ readonly inputEvents: NonJsonInputEvent<InputEvents>
292
+ }
293
+ }
294
+
249
295
  type ExcludeCompatibleRuntime<Requirements, Events, Emits> = Requirements extends Machine.Runtime.Requirement<
250
296
  infer RequiredEvents,
251
297
  infer RequiredEmits
@@ -303,6 +349,9 @@ export const layerMemory: Layer.Layer<Storage> = internal.layerMemory
303
349
  * rejected. Planning-time raised events remain part of the current macrostep.
304
350
  * Arbitrary action effects may run again after a crash before checkpoint
305
351
  * commit, so the bridge does not provide exactly-once external effects.
352
+ * State values, completion outputs, and public input events must declare
353
+ * JSON-compatible encoded representations. Local and internal event protocols
354
+ * remain unrestricted because they do not cross the Cluster boundary.
306
355
  *
307
356
  * **Example**
308
357
  *
@@ -364,7 +413,8 @@ export const make: <
364
413
  ParentEvents
365
414
  >
366
415
  & EnsureExecutable<States, UnhandledStates, OutputStates>
367
- & Machine.Machine.RootCompatible<ParentEvents>,
416
+ & Machine.Machine.RootCompatible<ParentEvents>
417
+ & EnsureJsonEncoded<States, InputEvents>,
368
418
  options: {
369
419
  readonly version: string
370
420
  },
@@ -582,7 +582,7 @@ type EnsureMachineExecutable<M extends Machine.Machine.Any> = IsAny<Machine.Mach
582
582
  >
583
583
 
584
584
  type MachineInputArgsOf<M extends Machine.Machine.Any> = [
585
- ...Machine.Machine.InputArgs<Machine.Machine.Input<M>>
585
+ ...Machine.Machine.InputArgs<Machine.Machine.InputSchema<M>>
586
586
  ]
587
587
 
588
588
  type MachineAtomOf<M extends Machine.Machine.Any, RuntimeError> = MachineAtom<